@use-gesture/react
@use-gesture/react turns browser pointer, touch, wheel, scroll, move, hover, and Safari gesture events into React hooks with a shared state model. Instead of manually calculating displacement, velocity, direction, pinch scale, swipe intent, start and end timing, or bounds, a handler receives those values on every update. It recognizes interaction; it does not animate pixels, reorder lists, or provide accessible drag-and-drop semantics, so it is usually paired with react-spring, Motion, or purpose-built UI logic.
A strong recognizer for custom, high-touch interfaces when your team understands browser input events. Do not install it for sortable drag and drop or to avoid learning touch-action, passive events, and accessibility behavior.
Use it if
- You are building direct-manipulation UI such as a canvas, slider, map, card stack, scrubber, or pinchable surface
- You need normalized movement, offset, velocity, direction, swipe, tap, and pinch state across input devices
- You want several gestures on one element through useGesture without maintaining overlapping event listeners
- You need bounds, axis locking, thresholds, rubberbanding, cancellation, or pointer-capture controls
- You are implementing sortable lists or keyboard-accessible drag and drop: this package recognizes low-level gestures but does not supply collision detection, reordering, announcements, or a complete accessibility model; use dnd-kit instead
- You expect animation or layout state out of the box: the README recommends pairing it with an animation library, and updating React state on every gesture frame can cause excessive rerenders
- You cannot own touch and browser-default behavior carefully: the docs require an appropriate touch-action rule for draggable elements and special handling for links, images, Safari pinch zoom, and passive listeners
- You need a visibly active release stream: 10.3.1 was published in March 2024 and the repository's last push was in July 2024, although it is not archived
- You only need a click, hover, or simple pointer handler: React's native event props are easier to read and avoid a gesture-state abstraction
Setup reality
Install @use-gesture/react and provide React 16.8 or newer; version 10.3.1 depends on the matching @use-gesture/core and ships its own declarations. There is no required animation peer, but that does not mean animation is solved. Gesture callbacks can fire for every input frame, so driving ordinary React state may rerender too much; use an imperative animation value, refs, or carefully scoped state for continuous movement. The first mobile surprise is CSS: a draggable surface needs touch-action that matches the interaction, commonly none for a small two-axis control or pan-y when vertical page scrolling must remain available. Without it, the browser can cancel pointer events when scrolling begins. Images and links also have native drag behavior, selection, and navigation, so upstream recommends user-select styling plus preventDefault and filterTaps where appropriate. React prop bindings cannot configure passive listeners. If a handler must cancel wheel, scroll, or Safari gesture events, attach through the target option and use eventOptions: { passive: false }; target should be a stable ref or EventTarget. Safari trackpad pinch uses proprietary GestureEvents that React does not expose, another reason to bind via target, and suppressing browser zoom has accessibility consequences. Drag captures the pointer by default, which keeps tracking outside the element but prevents other elements from receiving pointer events until release; set pointer.capture false for cross-target interactions. offset persists between gestures while movement resets each time, a distinction that commonly causes snapping. Bounds clamp those values, and rubberband has no effect without bounds. End handlers for wheel, scroll, and move are debounced because the DOM has no native end event. Hooks must still follow React's rules, refs are unavailable on the first render, and server-rendered components must keep browser-only targets out of server execution.
Patterns
Track a draggable elementdrag-element
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/web'
function Draggable() {
const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 }))
const bind = useDrag(({ offset: [x, y] }) => api.start({ x, y }))
return <animated.div {...bind()} style={{ x, y, touchAction: 'none' }} />
}offset persists across drags; movement resets to zero at the start of each new gesture.
Return to the origin on releasesnap-back-drag
const bind = useDrag(({ down, movement: [mx, my] }) => {
api.start({ x: down ? mx : 0, y: down ? my : 0, immediate: down })
})movement is the right state for a temporary displacement because it starts fresh for every drag.
Constrain and rubberband movementconstrain-drag
const bind = useDrag(
({ offset: [x, y] }) => api.start({ x, y }),
{
bounds: { left: -100, right: 100, top: -50, bottom: 50 },
rubberband: true,
},
)Rubberbanding only works with bounds, and the final event returns the value to the nearest bound.
Lock a gesture to its detected axislock-drag-axis
const bind = useDrag(
({ offset: [x, y] }) => api.start({ x, y }),
{ axis: 'lock', threshold: 8 },
)axis: lock chooses x or y after intent is detected; use axis: x when the component must always be horizontal.
Separate taps from dragsdistinguish-tap
const bind = useDrag(
({ tap, last }) => {
if (last && tap) openItem()
},
{ filterTaps: true },
)filterTaps prevents the drag handler from acting like a drag before the configured tap displacement is exceeded.
Move between pages on a horizontal swipedetect-swipe
const bind = useDrag(
({ swipe: [swipeX] }) => {
if (swipeX) setPage((page) => page - swipeX)
},
{
axis: 'x',
swipe: { distance: 50, velocity: 0.5, duration: 250 },
},
)A swipe is reported on release only when distance, velocity, and duration conditions all pass.
Track pinch scale and anglepinch-scale
import { usePinch } from '@use-gesture/react'
const bind = usePinch(
({ offset: [scale, angle] }) => api.start({ scale, rotateZ: angle }),
{
scaleBounds: { min: 0.5, max: 3 },
rubberband: true,
},
)Safari trackpad pinch needs target-based binding and browser gesture suppression because React does not expose WebKit GestureEvents.
Combine drag and pinch handlerscombine-gestures
import { useGesture } from '@use-gesture/react'
const bind = useGesture(
{
onDrag: ({ offset: [x, y] }) => api.start({ x, y }),
onPinch: ({ offset: [scale, angle] }) => api.start({ scale, rotateZ: angle }),
},
{
drag: { bounds: containerRef },
pinch: { scaleBounds: { min: 1, max: 4 } },
},
)useGesture config nests gesture-specific options under drag, pinch, wheel, scroll, or move keys.
Observe scrolling on a targetbind-window-scroll
import { useScroll } from '@use-gesture/react'
function ScrollProgress() {
useScroll(({ xy: [, y] }) => setProgress(y), { target: window })
return null
}Access window only in the browser; server-rendered applications should guard this component or pass a ref after mount.
Cancel a drag past a limitcancel-drag
const bind = useDrag(({ active, movement: [mx], cancel }) => {
if (mx > 200) cancel()
api.start({ x: active ? mx : 0, immediate: active })
})Only drag and pinch gestures implement cancel; calling it for move, scroll, wheel, or hover has no effect.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @dnd-kit/core | npm | You need accessible sortable or drag-and-drop UI with sensors, collision detection, and keyboard support |
| react-draggable | npm | You only need constrained element dragging and prefer a component-oriented API |
| framer-motion | npm | You want gestures and animation in one React package and accept a broader rendering abstraction |