Skip to main content

Swipe

Manages swipe interactions:

  • Tracks pointer movement and detects direction
  • Emits events on start, move, and end
  • Supports RAF-based release inertia with optional bounds bounce
  • Optional movement bounds, rubber-band overflow, and snap targets
  • Optional programmatic scale with zoom-to-point via setScale
  • Calculates rotation (angle in degrees)
  • Does not apply transforms — only provides computed coordinates

Coordinate spaces

Swipe exposes two related spaces in ISwipeCoords:

SpaceFieldsUse for
Pointerstart, prev, current, diff, step, accumGesture deltas, direction detection, velocity
Movementmovement, prevMovement, scaleElement transform when using bounds / snap / rubber

movement and scale are accumulated for the instance lifetime (not reset on each swipe). Per-gesture offsets live in diff and step.

With pointers > 1, pointer-space coordinates follow the center between active pointers (via internal Pointers.move), not the first touch only.

Basic Demo

HTML
<div class="fullpage-center">
<div class="wrapper">
<div id="all" class="item">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>

<div id="horizontal" class="item">
<img src="https://www.svgrepo.com/show/379996/drag-horizontal.svg" alt="">
</div>

<div id="vertical" class="item">
<img src="https://www.svgrepo.com/show/379997/drag-vertical.svg" alt="">
</div>

<div id="rotation" class="item">
<img src="https://www.svgrepo.com/show/533709/rotate.svg" alt="">
</div>
</div>
</div>
CSS
.wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}

.item {
width: 2.5rem;
height: 2.5rem;

display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;

border-radius: 0.5rem;

img {
width: 1.25rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}

#all {
background: var(--gradient-brand-135, #000);
}

#horizontal {
background: var(--gradient-brand-90, #000);
}

#vertical {
background: var(--gradient-brand-0, #000);
}

#rotation {
background: var(--gradient-brand-radial, #000);
}
JavaScript
import {
Swipe
} from "vevet";

// ALL

new Swipe({
container: document.getElementById("all"),
grabCursor: true,
onStart: (data, {
container
}) => {
container.style.zIndex = "1";
},
onMove: ({
diff
}, {
container
}) => {
container.style.transition = "";
container.style.transform = `translate(${diff.x}px, ${diff.y}px)`;
},
onEnd: (data, {
container
}) => {
container.style.zIndex = "";
container.style.transition = "transform 0.25s";
container.style.transform = "none";
}
});

// HORIZONTAL

new Swipe({
container: document.getElementById("horizontal"),
axis: "x",
grabCursor: true,
onStart: (data, {
container
}) => {
container.style.zIndex = "1";
},
onMove: ({
diff
}, {
container
}) => {
container.style.transition = "";
container.style.transform = `translate(${diff.x}px, 0)`;
},
onEnd: (data, {
container
}) => {
container.style.zIndex = "";
container.style.transition = "transform 0.25s";
container.style.transform = "none";
}
});

// VERTICAL

new Swipe({
container: document.getElementById("vertical"),
axis: "y",
grabCursor: true,
onStart: (data, {
container
}) => {
container.style.zIndex = "1";
},
onMove: ({
diff
}, {
container
}) => {
container.style.transition = "";
container.style.transform = `translate(0, ${diff.y}px)`;
},
onEnd: (data, {
container
}) => {
container.style.zIndex = "";
container.style.transition = "transform 0.25s";
container.style.transform = "none";
}
});

// ROTATION

new Swipe({
container: document.getElementById("rotation"),
relative: true,
grabCursor: true,
onMove: ({
diff
}, {
container
}) => {
container.style.transition = "";
container.style.transform = `rotate(${diff.angle}deg)`;
},
onEnd: (data, {
container
}) => {
container.style.transition = "transform 0.25s";
container.style.transform = "none";
}
});

More demos

To explore more demos, click here.

Initialization

caution
  • Swipe does not transform elements — apply movement, scale (or diff) yourself in callbacks.
  • With bounds / snap, prefer movement and scale for transforms; diff is pointer space only.
  • Set transform-origin on the transformed element (typically top left / 0 0) so translate and scale compose predictably.

Simple drag (pointer space):

import { Swipe } from 'vevet';

const swipe = new Swipe(
{
container: document.getElementById('container'),
grabCursor: true,
},
{
onMove: ({ diff }) => {
el.style.transform = `translate(${diff.x}px, ${diff.y}px)`;
},
onEnd: () => {
el.style.transform = '';
},
},
);

Bounded drag with rubber and inertia (movement space):

import { Swipe } from 'vevet';

const wrapper = document.getElementById('wrapper');
const thumb = document.getElementById('thumb');

const swipe = new Swipe({
container: wrapper,
thumb,
relative: true,
grabCursor: true,
inertia: true,
overflow: () => 50,
bounds: () => ({ x: [0, 300], y: [0, 300] }),
onMove: ({ movement }) => {
thumb.style.transform = `translate(${movement.x}px, ${movement.y}px)`;
},
});

// Optional initial offset
swipe.setMovement({ x: 150, y: 150 });

Image viewer with pan, wheel zoom, and scale-aware bounds (movement space):

import { addEventListener, clamp, Swipe } from 'vevet';

const wrapper = document.getElementById('wrapper');
const thumb = document.getElementById('thumb');

const minScale = 1;
const maxScale = 3;

const swipe = new Swipe({
container: wrapper,
thumb,
relative: true,
inertia: true,
grabCursor: true,
bounds: ({ scale }) => ({
x: [0, wrapper.clientWidth - thumb.clientWidth * scale],
y: [0, wrapper.clientHeight - thumb.clientHeight * scale],
}),
onMove: ({ movement, scale }) => {
thumb.style.transform = `translate(${movement.x}px, ${movement.y}px) scale(${scale})`;
},
});

addEventListener(
thumb,
'wheel',
(event) => {
event.preventDefault();

if (swipe.isSwiping) {
return;
}

swipe.setScale(
clamp(swipe.scale - event.deltaY * 0.001, minScale, maxScale),
event,
);
},
{ passive: false },
);

addEventListener(thumb, 'dblclick', (event) => {
const next =
swipe.scale >= maxScale
? minScale
: clamp(swipe.scale + 1, minScale, maxScale);

swipe.setScale(next, event);
});

Two-finger pan (touch) + pinch zoom via a separate Pointers instance:

import { clamp, Pointers, Swipe } from 'vevet';

const wrapper = document.getElementById('wrapper');
const thumb = document.getElementById('thumb');

const minScale = 1;
const maxScale = 10;

// Pan — one mouse pointer or two touch pointers
const swipe = new Swipe({
container: wrapper,
relative: true,
inertia: true,
pointers: (type) => (type === 'mouse' ? 1 : 2),
bounds: ({ scale }) => ({
x: [0, wrapper.clientWidth - thumb.clientWidth * scale],
y: [0, wrapper.clientHeight - thumb.clientHeight * scale],
}),
onMove: ({ movement, scale }) => {
thumb.style.transform = `translate(${movement.x}px, ${movement.y}px) scale(${scale})`;
},
});

// Pinch — zoom toward the midpoint between fingers
const pinch = new Pointers({
container: wrapper,
minPointers: 2,
maxPointers: 2,
relative: false, // client coords — matches Swipe.setScale origin decoding
onMove: ({ center, scale, prevScale }) => {
if (prevScale === scale) {
return;
}

swipe.setScale(
clamp(swipe.scale * (scale / prevScale), minScale, maxScale),
center,
);
},
onStart: () => swipe.updateProps({ inertia: false }),
onEnd: () => swipe.updateProps({ inertia: true }),
});
Coordinate space for pinch origin

Use a separate Pointers instance with relative: false and pass move.center to setScale. Coordinates are client-based; Swipe converts them to container space when relative is true.

Detect direction:

swipe.on('toLeft', () => console.log('swipe to left'));

Add inertia:

const swipe = new Swipe({
container: document.getElementById('container'),
inertia: true,
inertiaThreshold: 1, // px/s or deg/s minimum release speed
});

Destroy all events:

swipe.destroy();