Drag
DragEngine turns raw pointer and touch events into a rich, normalized gesture state — offset,
movement, velocity, direction and swipe — delivered to a single handler on every move. It rubberbands
past bounds, can cancel gestures that start on the wrong axis, detects taps, and manages the grab
cursor.
useDrag
useDrag
Drag the square — the offset is smoothed through
Motion
.
Usage
The handler receives the gesture state; feed state.offset wherever you want the movement to go.
Pairing it with Motion gives the drag inertia and smoothing:
import { useDrag } from '@60fps/react/drag-engine';
import { useMotion, useComposeAnimate } from '@60fps/react/motion';
function Draggable() {
const [, mx] = useMotion(0);
const [, my] = useMotion(0);
const ref = useComposeAnimate<HTMLDivElement>([mx, my], ([x, y], el) => {
el.style.transform = `translate(${x}px, ${y}px)`;
});
useDrag(ref, ({ offset }) => {
mx.set(offset.x);
my.set(offset.y);
});
return <div ref={ref} />;
}
<script setup lang="ts">
import { ref } from 'vue';
import { useDrag } from '@60fps/vue/drag-engine';
const el = ref<HTMLElement | null>(null);
const pos = reactive({ x: 0, y: 0 });
useDrag(el, ({ offset }) => {
pos.x = offset.x;
pos.y = offset.y;
});
</script>
<template>
<div ref="el" :style="{ translate: `${pos.x}px ${pos.y}px` }" />
</template>
Velocity & coordinates
Every move hands you the live gesture state. Here the box reports its offset (the drag
coordinates) and velocity (px/ms) each frame — flick it and let go to watch the release velocity
trip swipe:
Configurations
Each box is bounded to its field; the config controls how it moves. Bounds are computed inside a
config function so they survive resizes.
Rubberband, then spring back (default)
Dragged past the bounds, the box stretches elastically. On release the engine clamps offset back
into bounds and reports the overshoot in state.overflow — so you animate the element to the (now
clamped) offset only when overflow is non-zero (here with Motion):
useDrag(
ref,
({ offset, overflow, last }) => {
if (!last) {
motionX.set(offset.x, { immediate: true });
return;
}
// the engine clamped `offset` back into bounds; only spring if we overshot
if (overflow.x !== 0) motionX.set(offset.x, { easing: 'spring' });
},
{ rubber: true, minX: 0, maxX }
);
Constrain to one axis
There’s no path-locking option. axis only decides whether a gesture starts (see the config table);
to keep the box on one axis, clamp the other with bounds:
useDrag(ref, handler, { minY: 0, maxY: 0 }); // horizontal only
Hard clamp
Turn rubber off and the box stops dead at the bounds — no elasticity, so nothing to spring back.
useDrag(ref, handler, { rubber: false, minX: 0, maxX, minY: 0, maxY });
API
useDrag(ref, handler, config?)
Returns [clean, init]. The handler is called with the gesture state on press, every move, and
release.
Gesture state
| Field | Type | Default | Description |
|---|---|---|---|
offset | { x, y } | — | Accumulated position, persisted across drags. Rubberbands past the bounds while dragging (hard-clamps when rubber is off), and is clamped back into bounds on release. |
overflow | { x, y } | — | Signed overshoot per axis: negative when the offset is below min, positive when above max, 0 within bounds. Check it on release to decide whether to animate a snap-back. |
movement | { x, y } | — | Distance travelled within the current drag. |
delta | { x, y } | — | Change since the previous frame. |
velocity | { x, y } | — | Pointer velocity in px/ms. |
direction | { x, y } | — | Sign of travel on each axis (-1, 0, 1). |
swipe | { x, y } | — | Set to ±1 on release for a fast, short flick; 0 otherwise. |
first / last | boolean | — | Whether this is the first or final event of the gesture. |
tap | boolean | — | True when the pointer barely moved — treat as a click. |
cancel() | () => void | — | Abort the active gesture. |
Config
| Option | Type | Default | Description |
|---|---|---|---|
axis | 'x' | 'y' | — | Gates how the drag STARTS: the gesture is cancelled unless its initial movement is mostly along this axis (so e.g. a vertical page scroll passes through). It does not constrain movement once the drag is active. |
rubber | boolean | true | Rubberband when dragged past min/max. |
minX / maxX / minY / maxY | number | ∓Infinity | Movement bounds per axis. |
from | { x?, y? } | — | Starting offset, e.g. to resume from an externally-modified position. |
mouse / touch | boolean | true | Which input types trigger the gesture. |
cursor | boolean | — | Set grabbing cursor on the body during the drag. |
beforeStart / afterEnd | (state) => void | — | Lifecycle callbacks around the gesture. |