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;
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 dragging pastbounds.trueuses the default elastic constant; anumbersets a custom one;falsehard-clamps instead. Momentum on release always hard-stops atboundsregardless of this setting.momentumflings the element on release using the pointer's release velocity (viawithDecayunder the hood); setfalsefor a hard stop.
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.