Skip to main content
Version: next

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',
}}
/>
);
}
Preview

x and y are AnimateValues, so they can be read directly in style (as above) or combined/interpolated like any other animated value.

tip

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,
});
  • bounds accepts a fixed { left, right, top, bottom } box or a container ref to constrain against.
  • elastic controls the rubber-band feel while actively dragging past bounds. true uses the default elastic constant; a number sets a custom one; false hard-clamps instead.
  • momentum flings the element on release using the pointer's release velocity (via withDecay under the hood); set false for a hard stop. By default a momentum fling still hard-stops at bounds, unless bounce says otherwise.
Preview

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,
});
Preview

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

  • transition overrides the spring used to settle into bounds or snapPoints on release (it has no effect on the momentum fling itself, only springing back to a point). Accepts the same shape as flipOptions on animate.* (see flip transitions):
    useDrag(ref, {
    bounds: { left: -100, right: 100 },
    transition: withSpring({ stiffness: 500, damping: 30 }),
    });
  • decay tunes the deceleration constant used for the momentum fling (lower values add more friction, so the fling slows down faster). Defaults to 0.998, the same default withDecay uses.

Other options

  • axis: 'x' | 'y' locks movement to one axis.
  • minDistance requires 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, like useValue's initial value.
  • enabled: false disables the gesture without unmounting it.
  • onStart / onChange / onEnd receive the same PanEvent shape as Gesture.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, not left/top.
  • Prefer bounds + elastic/momentum over hand-rolling withSpring snap-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 minDistance to 0 if the element also handles clicks. Give clicks a little slop so they don't get swallowed as micro-drags.
  • Don't pass a new ref object 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 useDrag uses internally for momentum: true.
  • Reorder: the drag-to-reorder list component, for dragging items within a list instead of freely around the screen.