Skip to main content

Gestures adapter

@onlynative/inertia-gestures adds drag / swipe / pan hooks built on react-native-gesture-handler. It is an optional sibling package — install it only if you need richer-than-pressable gestures. The core library has no required gesture-handler dependency.

The hooks compose with any Motion.* primitive via a <GestureDetector> and a single style slot — they don't replace the gesture prop, they extend what's reachable beside it.

Install

yarn add @onlynative/inertia-gestures react-native-gesture-handler

Then follow the react-native-gesture-handler install guide — it needs <GestureHandlerRootView> near the root of your app.

useDrag

Drag a Motion primitive on one or two axes, with optional bounds and rubber-band overshoot.

import { GestureDetector } from 'react-native-gesture-handler'
import { Motion } from '@onlynative/inertia'
import { useDrag } from '@onlynative/inertia-gestures'

function DraggableBox() {
const drag = useDrag({
axis: 'both',
constraints: { left: -100, right: 100, top: -60, bottom: 60 },
elastic: 0.4,
})
return (
<GestureDetector gesture={drag.gesture}>
<Motion.View style={[styles.box, drag.animatedStyle]} />
</GestureDetector>
)
}
OptionTypeDefaultNotes
axis'x' | 'y' | 'both''both'Lock the gesture to one axis.
constraints{ left?, right?, top?, bottom? }noneBounds in px from the resting position. Each side independently optional.
elasticnumber (0–1)0Rubber-band coefficient past constraints. 0 hard-clamps; 0.4 is a typical Framer-Motion feel.
onDragStart() => voidnoneFires on JS thread when drag begins.
onDragEnd(info: { x, y, velocity: { x, y } }) => voidnoneFires on JS thread when drag ends.

Returns { gesture, animatedStyle, dragX, dragY, isDragging }. The shared values are exposed for power use cases (deriving secondary effects from drag position).

useSwipe

Directional commit-or-snap-back gesture. Tracks live translation while the user drags; on release, fires onSwipe if either the distance or velocity threshold is met along an allowed axis. Whether or not it commits, the position springs back to zero — the consumer drives the side effect (delete, dismiss, advance).

const swipe = useSwipe({
directions: ['left', 'right'],
distanceThreshold: 100,
onSwipe: (direction) => {
if (direction === 'left') dismiss()
if (direction === 'right') accept()
},
})

return (
<GestureDetector gesture={swipe.gesture}>
<Motion.View style={[styles.card, swipe.animatedStyle]}>
{children}
</Motion.View>
</GestureDetector>
)
OptionTypeDefaultNotes
directionsArray<'left' | 'right' | 'up' | 'down'>all fourOnly commits along listed directions. Off-axis swipes do nothing.
distanceThresholdnumber (px)80Distance past which a release commits.
velocityThresholdnumber (px/sec)800Flick velocity that commits even before distance threshold.
onSwipe(direction, { distance, velocity }) => voidnoneJS-thread callback. Direction is one of the allowed values.

The dominant axis (whichever of |tx|, |ty| is larger at release) decides which direction is checked.

usePan

Camera-style pan with momentum on release. Position persists across separate gestures — the next pan starts from the current offset, not zero — and on release the position continues gliding via Reanimated's withDecay.

const pan = usePan({
constraints: { left: -240, right: 240, top: -240, bottom: 240 },
deceleration: 0.997,
})

return (
<GestureDetector gesture={pan.gesture}>
<Motion.View style={[styles.canvas, pan.animatedStyle]}>
{/* large content */}
</Motion.View>
</GestureDetector>
)
OptionTypeDefaultNotes
constraints{ left?, right?, top?, bottom? }noneHard clamp during gesture and during decay.
decelerationnumberReanimated defaultHigher = momentum dies faster. Roughly 0.99 (slow) to 0.999 (long glide).
disableMomentumbooleanfalseHard stop on release (drag-like).

For rubber-banded constraints, prefer useDrag({ elastic })usePan hard-clamps because it composes with withDecay's clamp parameter, which doesn't have an overshoot mode.

When to pick which

GestureSnap back on release?Momentum on release?Typical UX
useDragNo — stays where leftNoMove-to-position, sliders, sortable handles.
useSwipeYes — alwaysNo (springs back)Swipe-to-delete rows, card stacks, dismiss sheets.
usePanNo — stays where leftYes — coasts via withDecayMaps, zoomable canvases, large-image navigation.

Try it

example / drag

Why a separate package?

The core library's gesture prop covers pressed / focused / focusVisible / hovered — boolean state-overlay gestures with no continuous translation. Drag, swipe, and pan are value-driven: the gesture publishes a continuous position that the consumer composes into the view's style. They don't fit the boolean-state model, and they need react-native-gesture-handler (which we don't want as a required peer of core).

Splitting them off keeps core's surface area minimal and lets the gesture-handler dependency stay opt-in. The hooks return primitives (gesture, animatedStyle, shared values) rather than wrapping Motion in a new component, so they layer cleanly on the existing Motion.* set.