useDrag
useDrag wires up Gesture.Pan() with live
position tracking, optional bounds, edge rubber-banding, and momentum on
release: everything you'd otherwise hand-roll with withSpring/withDecay
for a draggable element. Position persists across drags instead of resetting
each time.
Signature
function useDrag<T extends HTMLElement>(
ref: RefObject<T>,
options?: UseDragOptions
): UseDragResult;
interface UseDragOptions {
enabled?: boolean;
axis?: 'x' | 'y';
minDistance?: number;
initial?: { x?: number; y?: number };
bounds?: DragBounds | RefObject<HTMLElement>;
elastic?: boolean | number;
momentum?: boolean;
bounce?: boolean | number;
snapPoints?: { x?: number[]; y?: number[] };
transition?: FlipOptions;
decay?: number;
onStart?: (e: PanEvent) => void;
onChange?: (e: PanEvent) => void;
onEnd?: (e: PanEvent) => void;
}
interface DragBounds {
left?: number;
right?: number;
top?: number;
bottom?: number;
}
interface UseDragResult {
x: AnimateValue<number>;
y: AnimateValue<number>;
isDragging: boolean;
controls: Controls;
}
Basic usage
import { useRef } from 'react';
import { useDrag, animate } from 'react-ui-animate';
function DraggableBox() {
const ref = useRef(null);
/* x / y are AnimateValues — bind them to transforms */
const { x, y } = useDrag(ref);
return (
<animate.div
ref={ref}
style={{
translateX: x,
translateY: y,
width: 100,
height: 100,
background: '#60a5fa',
cursor: 'grab',
}}
/>
);
}
x and y are AnimateValues, so they can be read directly in style (as
above) or combined/interpolated like any other animated value.
Animate translateX / translateY, not left / top. Transforms stay on
the GPU and avoid layout thrash while dragging.
Bounds, elasticity, and momentum
/* Constrain drag, rubber-band at edges, fling on release */
useDrag(ref, {
bounds: { left: -100, right: 100, top: -100, bottom: 100 },
elastic: true,
momentum: true,
});
boundsaccepts a fixed{ left, right, top, bottom }box or a containerrefto constrain against.elasticcontrols the rubber-band feel while actively dragging pastbounds.trueuses the default elastic constant; anumbersets a custom one;falsehard-clamps instead.momentumflings the element on release using the pointer's release velocity (viawithDecayunder the hood); setfalsefor a hard stop. By default a momentum fling still hard-stops atbounds, unlessbouncesays otherwise.
Bouncing off bounds
bounce makes a momentum fling reflect off bounds instead of stopping
dead at them, like a ball off a wall, losing some speed on each bounce:
useDrag(ref, {
bounds: { left: -80, right: 80, top: -80, bottom: 80 },
bounce: true,
});
true uses a default restitution of 0.5 (each bounce keeps half the
speed); pass a number between 0 and 1 for a custom restitution, or
false (the default) for the regular hard-stop-at-bounds behavior. bounce
only affects the momentum fling, not clicking/dragging past bounds while
actively pressed, that's still elastic's job.
Snapping to fixed points
snapPoints settles the drag onto the nearest of a fixed list of positions
on release, per axis, instead of wherever bounds/momentum would
otherwise land it:
useDrag(ref, {
axis: 'x',
snapPoints: { x: [0, 120, 240] },
});
The nearest point is chosen by projecting the release velocity forward
first (the same snapTo helper Reorder
and the rest of the library use), so a fast flick snaps to the next point
in that direction rather than always the closest one at release. When set
for an axis, snapPoints takes priority over bounds/momentum for that
axis.
Custom release transition and decay
transitionoverrides the spring used to settle intoboundsorsnapPointson release (it has no effect on the momentum fling itself, only springing back to a point). Accepts the same shape asflipOptionsonanimate.*(see flip transitions):useDrag(ref, {
bounds: { left: -100, right: 100 },
transition: withSpring({ stiffness: 500, damping: 30 }),
});decaytunes the deceleration constant used for the momentum fling (lower values add more friction, so the fling slows down faster). Defaults to0.998, the same defaultwithDecayuses.
Other options
axis: 'x' | 'y'locks movement to one axis.minDistancerequires the pointer to travel that many pixels before the drag is recognized (useful to avoid hijacking clicks/taps).initial: { x, y }sets the starting position, read once on mount, likeuseValue's initial value.enabled: falsedisables the gesture without unmounting it.onStart/onChange/onEndreceive the samePanEventshape asGesture.Pan(): use these when you need side effects beyond position (e.g. triggering a dismiss action).
Multiple draggable elements
useDrag takes a single ref. For several independent draggables, call it
once per element (typically by extracting a small child component), rather
than passing an array. Each call gets its own bounds/position/state.
isDragging and controls
const { x, y, isDragging, controls } = useDrag(ref);
isDragging is true while actively pressed and moving. controls.reset() /
.cancel() / .pause() / .resume() act on the current release animation
(spring-to-bounds or momentum decay).
Best practices
Do
- Animate
translateX/translateY, notleft/top. - Prefer
bounds+elastic/momentumover hand-rollingwithSpringsnap-back logic. It's the same physics, already wired up. - Provide a visual cue for draggability (
cursor: 'grab'/'grabbing') and a non-gesture fallback where the interaction matters (e.g. buttons alongside a draggable slider).
Don't
- Don't set
minDistanceto0if the element also handles clicks. Give clicks a little slop so they don't get swallowed as micro-drags. - Don't pass a new
refobject on every render. A stable ref is required since registration happens once on mount.
Next steps
- useGesture: the lower-level
Gesture.Pan()builder this hook is built on, plus the other seven gesture types. - withDecay: the momentum animation
useDraguses internally formomentum: true. - Reorder: the drag-to-reorder list component, for dragging items within a list instead of freely around the screen.