Demos
Basic Demo
Overview of the main drag modes in one place: free movement, horizontal, vertical, and rotation.
Each instance uses pointer-space diff and resets the transform on release — no movement persistence.
Key props: grabCursor: true, axis: "x" / axis: "y", relative: true (rotation).
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";
}
});
Multidirectional Swipe
Free drag on both axes without an axis lock.
Applies diff.x and diff.y in onMove; the element snaps back on onEnd.
HTML
<div class="fullpage-center">
<div id="all" class="item">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.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);
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("all"),
grabCursor: true,
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";
}
});
Two-fingers Pan
Two-finger pan on touch devices. Movement follows the center between both pointers.
Key prop: pointers: 2. Uses diff and resets on release — same pattern as Multidirectional Swipe.
HTML
<div class="fullpage-center">
<div id="all" class="item">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.item {
width: 4rem;
height: 4rem;
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);
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("all"),
grabCursor: true,
pointers: 2,
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";
}
});
Right-Click Drag
Drag starts only with the right mouse button.
Key prop: buttons: [2].
HTML
<div class="fullpage-center">
<div id="all" class="item">
<img src="https://www.svgrepo.com/show/119214/right-click-of-the-mouse.svg" alt="">
</div>
</div>
CSS
.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-radial, #000);
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("all"),
grabCursor: true,
buttons: [2],
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 Swipe
Horizontal drag locked to the x-axis.
Key prop: axis: "x". Uses diff.x and resets on release.
HTML
<div class="fullpage-center">
<div id="horizontal" class="item">
<img src="https://www.svgrepo.com/show/379996/drag-horizontal.svg" alt="">
</div>
</div>
CSS
.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;
}
}
#horizontal {
background: var(--gradient-brand-90, #000);
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("horizontal"),
axis: "x",
grabCursor: true,
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 Swipe
Vertical drag locked to the y-axis.
Key prop: axis: "y". Uses diff.y and resets on release.
HTML
<div class="fullpage-center">
<div id="vertical" class="item">
<img src="https://www.svgrepo.com/show/379997/drag-vertical.svg" alt="">
</div>
</div>
CSS
.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;
}
}
#vertical {
background: var(--gradient-brand-0, #000);
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("vertical"),
axis: "y",
grabCursor: true,
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 Swipe
Angular drag around the element center.
Key props: relative: true, diff.angle. Rotation resets on release.
HTML
<div class="fullpage-center">
<div id="rotation" class="item">
<img src="https://www.svgrepo.com/show/533709/rotate.svg" alt="">
</div>
</div>
CSS
.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;
}
}
#rotation {
background: var(--gradient-brand-radial, #000);
}
JavaScript
import {
Swipe
} from "vevet";
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";
}
});
Slide Right
Horizontal slider thumb inside a track. Position persists across gestures via movement.
Key props: thumb, bounds, inertia: true, grabCursor: true.
Bounds are recalculated on resize with calculateBounds() and releaseBounce().
HTML
<div class="fullpage-center">
<div id="container" class="container">
<div id="thumb" class="thumb"><img src="https://www.svgrepo.com/show/379996/drag-horizontal.svg" alt=""></div>
</div>
</div>
CSS
.container {
width: 80%;
height: 3rem;
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.1);
}
.thumb {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: inherit;
background: var(--gradient-brand-90);
img {
width: 1.5rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
onResize
} from "vevet";
const container = document.getElementById("container");
const thumb = document.getElementById("thumb");
const swipe = new Swipe({
container,
thumb,
inertia: true,
grabCursor: true,
bounds: () => ({
x: [0, container.clientWidth - thumb.clientWidth]
}),
onMove: ({
movement
}, {
container
}) => {
thumb.style.transform = `translate(${movement.x}px, 0)`;
}
});
onResize({
element: [container],
callback: () => {
swipe.calculateBounds();
swipe.releaseBounce(0);
},
resizeDebounce: 100
});
Slide Down
Vertical slider variant — same pattern as Slide Right, but with y-axis bounds.
HTML
<div class="fullpage-center">
<div id="container" class="container">
<div id="thumb" class="thumb"><img src="https://www.svgrepo.com/show/379997/drag-vertical.svg" alt=""></div>
</div>
</div>
CSS
.container {
height: 300px;
width: 3rem;
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.1);
}
.thumb {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: inherit;
background: var(--gradient-brand-0);
img {
width: 1.5rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe
} from "vevet";
const container = document.getElementById("container");
const thumb = document.getElementById("thumb");
const swipe = new Swipe({
container,
thumb,
inertia: true,
grabCursor: true,
bounds: () => ({
y: [0, container.clientHeight - thumb.clientHeight]
}),
onMove: ({
movement
}, {
container
}) => {
thumb.style.transform = `translate(0, ${movement.y}px)`;
}
});
Lever rotation
Radial lever control: drag the handle (thumb) to rotate the dial. Angle is measured relative to the container center.
Key props: thumb, relative: true, inertia: true. Rotation accumulates via step.angle.
HTML
<div class="fullpage-center">
<div id="rotation" class="item">
<img src="https://www.svgrepo.com/show/533709/rotate.svg" alt="">
<div id="thumb" class="thumb"></div>
</div>
</div>
CSS
.item {
position: relative;
width: 6rem;
height: 6rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--gradient-sunset, #000);
img {
width: 2rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
.thumb {
position: absolute;
top: -0.6rem;
left: calc(50% - 0.6rem);
width: 1.2rem;
height: 1.2rem;
background: var(--gradient-brand-0, #000);
border-radius: 50%;
cursor: grab;
}
JavaScript
import {
Swipe
} from "vevet";
let angle = 0;
new Swipe({
container: document.getElementById("rotation"),
thumb: document.getElementById("thumb"),
inertia: true,
relative: true,
onMove: ({
step
}, {
container
}) => {
angle += step.angle;
container.style.transform = `rotate(${angle}deg)`;
}
});
Detect Swipe Direction
Directional callbacks fire after the gesture ends once directionThreshold is met.
Callbacks: onStart, onToLeft, onToRight, onToTop, onToBottom.
HTML
<div class="fullpage-center">
<div id="container" class="container">
Swipe Me
</div>
</div>
CSS
.container {
width: 7rem;
height: 7rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: 1rem;
background: var(--gradient-brand-135, #000);
font-size: 0.75rem;
}
JavaScript
import {
Swipe
} from "vevet";
new Swipe({
container: document.getElementById("container"),
onStart: (data, {
container
}) => {
container.innerHTML = "start";
},
onToLeft: (data, {
container
}) => {
container.innerHTML = "to left";
},
onToRight: (data, {
container
}) => {
container.innerHTML = "to right";
},
onToTop: (data, {
container
}) => {
container.innerHTML = "to top";
},
onToBottom: (data, {
container
}) => {
container.innerHTML = "to bottom";
}
});
Drag Inertia
Release inertia without built-in bounds — limits are applied manually in onMove.
Key props: inertia: true, grabCursor: true. Position is accumulated from step and clamped with clamp().
HTML
<div class="fullpage-center" id="page">
<div id="container" class="container">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.fullpage-center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
}
.container {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: var(--gradient-brand-135, #000);
border-radius: 0.5rem;
img {
width: 1.25rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
clamp
} from "vevet";
const page = document.getElementById("page");
let x = 0;
let y = 0;
new Swipe({
container: document.getElementById("container"),
inertia: true,
grabCursor: true,
onMove: ({
step
}, {
container
}) => {
const pageWidth = page.clientWidth;
const pageHeight = page.clientHeight;
x = clamp(x + step.x, -pageWidth / 2, pageWidth / 2);
y = clamp(y + step.y, -pageHeight / 2, pageHeight / 2);
container.style.transform = `translate(${x}px, ${y}px)`;
}
});
Drag Inertia Bounce
Movement-space drag inside a wrapper with automatic bounds and rubber-band overflow.
Key props: bounds, overflow, inertia: true, movement for transforms.
HTML
<div class="fullpage-center">
<div id="wrapper" class="wrapper">
<div id="container" class="container">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
</div>
CSS
.wrapper {
position: relative;
width: 10rem;
height: 10rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
}
.container {
width: 2rem;
height: 2rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: var(--gradient-brand-135, #000);
border-radius: 0.25rem;
img {
width: 1rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
vevet,
clamp,
EaseOutBack
} from "vevet";
const wrapper = document.getElementById("wrapper");
const container = document.getElementById("container");
new Swipe({
container: document.getElementById("container"),
grabCursor: true,
inertia: true,
overflow: () => 20,
bounds: () => ({
x: [
(wrapper.clientWidth - container.clientWidth) * -0.5,
(wrapper.clientWidth - container.clientWidth) * 0.5
],
y: [
(wrapper.clientHeight - container.clientHeight) * -0.5,
(wrapper.clientHeight - container.clientHeight) * 0.5
]
}),
onMove: ({
movement
}, {
container
}) => {
container.style.transform = `translate(${movement.x}px, ${movement.y}px)`;
}
});
Bouncing Ball
Custom physics on top of release inertia: the ball reverses direction when it hits the viewport edges.
Uses hasInertia in onMove to flip direction only during the inertia phase.
HTML
<div class="fullpage-center" id="page">
<div id="ball" class="ball"></div>
</div>
CSS
.fullpage-center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
}
.ball {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: var(--gradient-sunset, #000);
}
JavaScript
import {
Swipe,
clamp
} from "vevet";
const page = document.getElementById("page");
let x = 0;
let y = 0;
let xDir = 1;
let yDir = 1;
new Swipe({
container: document.getElementById("ball"),
inertia: true,
grabCursor: true,
onStart: () => {
xDir = 1;
yDir = 1;
},
onMove: ({
step
}, {
hasInertia,
container
}) => {
const pageWidth = page.clientWidth;
const pageHeight = page.clientHeight;
if (hasInertia) {
if (x >= pageWidth / 2 || x <= -pageWidth / 2) {
xDir *= -1;
}
if (y >= pageHeight / 2 || y <= -pageHeight / 2) {
yDir *= -1;
}
}
x = clamp(x + step.x * xDir, -pageWidth / 2, pageWidth / 2);
y = clamp(y + step.y * yDir, -pageHeight / 2, pageHeight / 2);
container.style.transform = `translate(${x}px, ${y}px)`;
}
});
Vertical Scrolling
Simulated vertical scroll with CSS translateY instead of native overflow.
Key props: bounds on the y-axis, inertia: true, movement.y, grabCursor: true.
HTML
<div class="fullpage-center">
<div id="container" class="container">
<div id="grid" class="grid">
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit</p>
</div>
</div>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.container {
width: 100%;
height: calc(100svh - 2rem);
border-radius: 0.5rem;
overflow: hidden;
background: rgba(255, 255, 255, 0.1);
}
.grid {
display: grid;
gap: 1rem;
padding: 1rem;
}
p {
margin: 0;
padding: 1rem;
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.1);
font-size: 0.75rem;
}
.thumb {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-radius: inherit;
background: var(--gradient-brand-0);
img {
width: 1.5rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
vevet,
clamp,
onResize
} from "vevet";
const container = document.getElementById("container");
const grid = document.getElementById("grid");
const instance = new Swipe({
container,
inertia: true,
grabCursor: true,
bounds: () => ({
y: [0, -(grid.clientHeight - container.clientHeight)]
}),
onMove: ({
movement
}) => {
grid.style.transform = `translateY(${movement.y}px)`;
}
});
Fullpage Bounds
Drag an element anywhere on the page, constrained to the viewport center.
Key props: bounds relative to the page size, inertia: true, movement. Recalculates bounds on resize.
HTML
<div class="fullpage-center" id="page">
<div id="container" class="container">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.fullpage-center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
}
.container {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: var(--gradient-brand-135, #000);
border-radius: 0.5rem;
img {
width: 1.25rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
onResize
} from "vevet";
const page = document.getElementById("page");
const swipe = new Swipe({
container: document.getElementById("container"),
inertia: true,
grabCursor: true,
bounds: () => ({
x: [
(page.clientWidth - container.clientWidth) / -2,
(page.clientWidth - container.clientWidth) / 2
],
y: [
(page.clientHeight - container.clientHeight) / -2,
(page.clientHeight - container.clientHeight) / 2
]
}),
onMove: ({
movement
}, {
container
}) => {
container.style.transform = `translate(${movement.x}px, ${movement.y}px)`;
}
});
onResize({
elements: [page],
callback: () => {
swipe.calculateBounds();
swipe.releaseBounce(0);
},
resizeDebounce: 100
});
Inertia Distance Modifier
Caps how far release inertia can travel.
Key prop: inertiaDistanceModifier — clamps x/y distance to ±100 px.
HTML
<div class="fullpage-center" id="page">
<div id="container" class="container">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.fullpage-center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: hidden;
}
.container {
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: var(--gradient-brand-135, #000);
border-radius: 0.5rem;
img {
width: 1.25rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
onResize,
clamp
} from "vevet";
const page = document.getElementById("page");
const swipe = new Swipe({
container: document.getElementById("container"),
inertia: true,
grabCursor: true,
bounds: () => ({
x: [
(page.clientWidth - container.clientWidth) / -2,
(page.clientWidth - container.clientWidth) / 2
],
y: [
(page.clientHeight - container.clientHeight) / -2,
(page.clientHeight - container.clientHeight) / 2
]
}),
inertiaDistanceModifier: distance => {
const limit = 100;
return {
x: clamp(distance.x, -limit, limit),
y: clamp(distance.y, -limit, limit),
angle: 0
};
},
onMove: ({
movement
}, {
container
}) => {
container.style.transform = `translate(${movement.x}px, ${movement.y}px)`;
}
});
onResize({
elements: [page],
callback: () => {
swipe.calculateBounds();
swipe.releaseBounce(0);
},
resizeDebounce: 100
});
Inertia Settings
Live controls for tuning release inertia feel.
Adjust inertiaRatio, inertiaDecay, and inertiaBounceEase via updateProps().
HTML
<div class="fullpage-center" id="page">
<div class="settings">
<div>
ratio <input id="ratio" type="range" min="0" max="1" step="0.001" value="1">
</div>
<div>
decay <input id="decay" type="range" min="0.001" max="1" step="0.001" value="0.05">
</div>
<div>
bounceEase <input id="bounceEase" type="range" min="0.001" max="1" step="0.001" value="0.3">
</div>
</div>
<div id="container" class="container">
<img src="https://www.svgrepo.com/show/521614/drag.svg" alt="">
</div>
</div>
CSS
.fullpage-center {
position: relative;
padding: 1rem;
}
.settings {
align-self: start;
}
.container {
position: absolute;
top: calc(50% - 1.5rem);
left: calc(50% - 1.5rem);
width: 3rem;
height: 3rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: var(--gradient-brand-135, #000);
border-radius: 0.5rem;
img {
width: 1.25rem;
height: auto;
filter: invert(100%);
background: transparent;
}
}
JavaScript
import {
Swipe,
onResize,
clamp
} from "vevet";
const page = document.getElementById("page");
const swipe = new Swipe({
container: document.getElementById("container"),
inertia: true,
grabCursor: true,
bounds: () => ({
x: [
(page.clientWidth - container.clientWidth) / -2,
(page.clientWidth - container.clientWidth) / 2
],
y: [
(page.clientHeight - container.clientHeight) / -2,
(page.clientHeight - container.clientHeight) / 2
]
}),
onMove: ({
movement
}, {
container
}) => {
container.style.transform = `translate(${movement.x}px, ${movement.y}px)`;
}
});
onResize({
element: [page],
callback: () => {
swipe.calculateBounds();
swipe.releaseBounce(0);
},
resizeDebounce: 100
});
document.getElementById('ratio').addEventListener('input', evt => {
swipe.updateProps({
inertiaRatio: parseFloat(evt.currentTarget.value)
});
});
document.getElementById('decay').addEventListener('input', evt => {
swipe.updateProps({
inertiaDecay: parseFloat(evt.currentTarget.value)
});
});
document.getElementById('bounceEase').addEventListener('input', evt => {
swipe.updateProps({
inertiaBounceEase: parseFloat(evt.currentTarget.value)
});
});
Image Viewer
Image viewer with pan, zoom, and pinch support. Combines Swipe with Pointers.
- Pan:
movement+boundsthat depend onscale - Zoom: mouse wheel, double-click, and two-finger pinch via
setScale() relative: truefor zoom-to-point
HTML
<div class="fullpage-center">
<div class="container" id="container">
<img class="image" id="image" src="https://fastly.picsum.photos/id/314/1920/1080.jpg?hmac=QAv7htpuXdwRq9YJDaEo_1mEiiAkEJeJFoBlSp-l7VY" alt="">
</div>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.container {
position: relative;
z-index: 1;
width: 100%;
height: min(70svh, 400px);
overflow: hidden;
background: rgba(255, 255, 255, 0.1);
}
.image {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
transform-origin: top left;
}
JavaScript
import {
addEventListener,
clamp,
onResize,
Pointers,
Swipe
} from "vevet";
const maxScale = 5;
const minScale = 1;
const stepScale = 1;
const container = document.getElementById("container");
const image = document.getElementById("image");
function getImageSize() {
return {
width: image.clientWidth,
height: image.clientHeight
};
}
function getContainerSize() {
return {
width: container.clientWidth,
height: container.clientHeight
};
}
const swipe = new Swipe({
container,
pointers: 1,
relative: true,
inertia: true,
grabCursor: true,
buttons: [0],
inertiaRatio: 1,
bounds: ({
scale
}) => {
const containerSize = getContainerSize();
const imageSize = getImageSize();
return {
x: [0, containerSize.width - imageSize.width * scale],
y: [0, containerSize.height - imageSize.height * scale]
};
},
onMove: ({
movement,
scale
}) => {
image.style.transform = `translate(${movement.x}px, ${movement.y}px) scale(${scale})`;
}
});
addEventListener(
image,
"wheel",
evt => {
evt.preventDefault();
if (swipe.isSwiping) {
return;
}
swipe.cancelInertia();
swipe.setScale(
clamp(swipe.scale - evt.deltaY * 0.001, minScale, maxScale),
evt);
}, {
passive: false
});
addEventListener(image, "dblclick", evt => {
if (swipe.scale >= maxScale) {
swipe.setScale(minScale, evt);
} else {
swipe.setScale(clamp(swipe.scale + stepScale, minScale, maxScale), evt);
}
});
onResize({
element: [container, image],
callback: () => {
if (swipe.scale === minScale) {
const containerSize = getContainerSize();
const imageSize = getImageSize();
swipe.setMovement({
x: (containerSize.width - imageSize.width) / 2,
y: (containerSize.height - imageSize.height) / 2
});
}
swipe.calculateBounds();
swipe.releaseBounce(0);
}
});
new Pointers({
container,
minPointers: 2,
maxPointers: 2,
relative: false,
onMove: ({
prevScale,
scale,
startCenter
}) => {
if (prevScale === scale) {
return;
}
swipe.setScale(
clamp(swipe.scale * (scale / prevScale), minScale, maxScale),
startCenter);
},
onStart: () => {
swipe.updateProps({
enabled: false,
inertia: false
});
},
onEnd: () => {
setTimeout(() => {
swipe.updateProps({
enabled: true,
inertia: true
});
});
}
});
Image Viewer Right Click
Same image viewer, but pan is triggered differently per input type:
- Mouse: right button (
buttons: [2]) - Touch: two fingers (
pointers: 2)
HTML
<div class="fullpage-center">
<div class="container" id="container">
<img class="image" id="image" src="https://fastly.picsum.photos/id/314/1920/1080.jpg?hmac=QAv7htpuXdwRq9YJDaEo_1mEiiAkEJeJFoBlSp-l7VY" alt="">
</div>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.container {
position: relative;
z-index: 1;
width: 100%;
height: min(70svh, 400px);
overflow: hidden;
background: rgba(255, 255, 255, 0.1);
}
.image {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
transform-origin: top left;
}
JavaScript
import {
addEventListener,
clamp,
onResize,
Pointers,
Swipe
} from "vevet";
const maxScale = 5;
const minScale = 1;
const stepScale = 1;
const container = document.getElementById("container");
const image = document.getElementById("image");
function getImageSize() {
return {
width: image.clientWidth,
height: image.clientHeight
};
}
function getContainerSize() {
return {
width: container.clientWidth,
height: container.clientHeight
};
}
const swipe = new Swipe({
container,
pointers: type => type === "mouse" ? 1 : 2,
relative: true,
inertia: true,
grabCursor: true,
buttons: type => type === "touch" ? [0] : [2],
inertiaRatio: 0.1,
bounds: ({
scale
}) => {
const containerSize = getContainerSize();
const imageSize = getImageSize();
return {
x: [0, containerSize.width - imageSize.width * scale],
y: [0, containerSize.height - imageSize.height * scale]
};
},
onMove: ({
movement,
scale
}) => {
image.style.transform = `translate(${movement.x}px, ${movement.y}px) scale(${scale})`;
}
});
addEventListener(
image,
"wheel",
evt => {
evt.preventDefault();
if (swipe.isSwiping) {
return;
}
swipe.cancelInertia();
swipe.setScale(
clamp(swipe.scale - evt.deltaY * 0.001, minScale, maxScale),
evt);
}, {
passive: false
});
addEventListener(image, "dblclick", evt => {
if (swipe.scale >= maxScale) {
swipe.setScale(minScale, evt);
} else {
swipe.setScale(clamp(swipe.scale + stepScale, minScale, maxScale), evt);
}
});
onResize({
element: [container, image],
callback: () => {
if (swipe.scale === minScale) {
const containerSize = getContainerSize();
const imageSize = getImageSize();
swipe.setMovement({
x: (containerSize.width - imageSize.width) / 2,
y: (containerSize.height - imageSize.height) / 2
});
}
swipe.calculateBounds();
swipe.releaseBounce(0);
}
});
new Pointers({
container,
minPointers: 2,
maxPointers: 2,
relative: false,
onMove: ({
prevScale,
scale,
startCenter
}) => {
if (prevScale === scale) {
return;
}
swipe.setScale(
clamp(swipe.scale * (scale / prevScale), minScale, maxScale),
startCenter);
}
});