Skip to content
60fps/ui

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:

released
drag
offset (x, y)
0, 0 px
velocity (x, y)
0.00, 0.00 px/ms
speed0.00
released
drag
offset (x, y)
0, 0 px
velocity (x, y)
0.00, 0.00 px/ms
speed0.00

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.