Declarative gesture primitives for React Native, built as a thin, ergonomic wrapper around react-native-gesture-handler. A gesture is written as an intent — useSwipe, useDoubleTap, usePinch — not assembled from a builder chain, a useMemo, a ref dance, and hand-written translation maths.
Status:
0.0.0-alpha.1— first alpha, published on thealphadist-tag. Milestone 1's foundation is in place:useGestures, thealongside/blocks/deferTocoexistence options,useRawGesture, and gesture identity that is stable by construction.useTap,useDoubleTap,useLongPress,useDrag,usePan,useSwipe,usePinch, anduseRotateare the intent hooks so far.useHoveranduseEdgeSwipeare designed but not written — read them below as decisions that have been made, not as shipped features. Milestone 1's code is complete, and the mechanical half of its gate passed on Android and iOS on 2026-09-19. How a gesture feels is still untested, so the activation-criteria defaults an injected touch could not reach are design intentions.
rootnative.github.io/impulse — installation, the six intent hooks, composition, coexistence, and the web behaviour of each intent.
- Installation
useTap,useDoubleTap,useLongPress,useDrag,usePan,useSwipe,usePinch,useRotate- Composition and Coexistence
- Web behaviour — what was measured in a browser, and what nobody has checked
- Roadmap
- The example app runs in a browser at /impulse/example/.
For an AI agent: AI agents and llms.txt explains the two files, llms.txt and llms-full.txt. After you install the package, node_modules/@rootnative/impulse/llms.txt is the exact API of that version.
RNGH is a good library, and it is not the problem. It deliberately exposes mechanisms — activation criteria, gesture relations, state machines — and leaves every intent to the consumer. So each app rebuilds swipe-to-dismiss, double-tap, long-press-then-drag, pinch-about-a-focal-point, and edge-swipe from Pan plus arithmetic, and each rebuild gets the sharp edges wrong in its own way.
// RNGH today
const pan = useMemo(
() =>
Gesture.Pan()
.activeOffsetX([-10, 10])
.failOffsetY([-5, 5])
.simultaneousWithExternalGesture(scrollRef)
.onUpdate((e) => {
x.value = e.translationX
})
.onEnd((e) => {
runOnJS(commit)(e.translationX > 100)
}),
[],
)
// Impulse
const swipe = useSwipe({
directions: ['left', 'right'],
onSwipe: (e) => commit(e.direction === 'right'),
})Impulse is general-purpose. It is not built for, owned by, or coupled to any specific consumer, including @rootnative/ui and @rootnative/inertia. The @rootnative scope is a publishing namespace and nothing more.
Impulse detects, Inertia animates. Impulse owns gesture recognition, composition, coexistence, and the shared values a gesture produces. @rootnative/inertia owns the transition vocabulary and everything that turns a velocity into motion.
Impulse peers on gesture-handler and Reanimated only. Inertia interop will arrive as an optional subpath (@rootnative/impulse/inertia) and will never become a required dependency.
That independence is why this is a separate repository rather than a package inside inertia. Two rules in that repository's release guard make it impossible there: every non-private package is pinned to core's version, and every adapter must declare a narrow @rootnative/inertia peer. Both are correct for an adapter and fatal for a general-purpose gesture library, which has to release on its own cadence and has to be usable without an animation library.
| Path | What it is |
|---|---|
| packages/core/ | @rootnative/impulse — the whole public surface. |
| docs/ | Docusaurus site, private. Deployed to rootnative.github.io/impulse, and it carries the web build of the example app. |
| example/ | Expo app for manual validation, one screen per intent. A device pass is a release requirement, not a nicety. |
| scripts/ | build-llms.mjs, check-versions.mjs, and build-brand.mjs. |
Inside packages/core/src:
| Path | What it is |
|---|---|
compose/ |
useGestures — the only place RNGH's Race / Simultaneous / Exclusive are referenced. |
relations/ |
alongside / blocks / deferTo resolution to RNGH's three external-gesture relations. One module, so the three-way choice is decided once. |
raw/ |
useRawGesture, the mechanism-level escape hatch. |
internal/ |
Not public. The shared memoisation helper, useLatestCallback, useStableList, and keyed dev warnings. |
gesture-handler/ |
The RNGH interop subpath — pure re-exports, pinned to reference identity by a test. |
Requires Node 20 or later and pnpm 10.
pnpm install
pnpm run build # build packages (Turborepo)
pnpm run dev # watch mode
pnpm run typecheck # tsc --noEmit across packages
pnpm run test # Jest
pnpm run lint # ESLint
pnpm run format # Prettier
pnpm run example # Expo dev server
pnpm run docs:dev # Docusaurus dev server
pnpm run build:llms # regenerate llms.txt and llms-full.txt after a docs change
pnpm run check:versions # verify every version reference agrees with core'sTo run one test, target the package first:
pnpm --filter @rootnative/impulse test -- gestureIdentityExpo SDK 57 — React 19.2.3, React Native 0.86.3, Reanimated 4.5.1, Worklets 0.10.1, Gesture Handler 2.32.0. This is the same band as inertia, ui, ui-example, and rootnative.
Both build on gesture-handler and both ship a useDrag, a usePan, and a useSwipe, so the names collide. The products do not: -gestures requires @rootnative/inertia and hands back an animatedStyle that already knows about bounds and spring-back, while Impulse requires no animation library and hands back the gesture and its values. Reach for -gestures to make a Motion primitive follow a finger; reach for Impulse for the gesture itself. -gestures is not deprecated.
- Composition and coexistence core —
useGestureswith all three modes,alongside/blocks/deferToresolution,useRawGesture, the memoisation helper,useTap, anduseDragare done. The mechanical half of the device pass that closes this milestone is done on both platforms; feel is not. - The intent set —
useDoubleTap,useLongPress,usePan,useSwipe,usePinch, anduseRotateare done.useHoveranduseEdgeSwiperemain. - The Inertia bridge —
@rootnative/impulse/inertia, adapting a release payload into Inertia's release transitions.
Each milestone has a graduation gate, and every gate needs a pass on real hardware — a test runner cannot tell you whether a horizontal drag inside a vertical ScrollView feels right.
MIT © RootNative. See LICENSE.
