Skip to main content
React Native · gesture-handler 2

Declarative gestures for React Native.

Declarative gesture primitives for React Native. A thin wrapper around react-native-gesture-handler — you write a gesture as an intent, not as a builder chain, a useMemo, a ref dance, and hand-written translation maths.

Alpha. Four intents ship today — useTap, useDoubleTap, useLongPress, and useDrag. The rest are designed and not written. See the roadmap.

4Intents shipped
0Builder chains
100%TypeScript
MITLicensed

Get started in 3 steps

Your app must render a GestureHandlerRootView above the component. The installation page has the full setup.

01

Install

Add Impulse, gesture-handler, and Reanimated.

yarn add @rootnative/impulse react-native-gesture-handler
02

Import

One intent hook, plus the detector that renders it.

import { useTap } from '@rootnative/impulse'
03

Detect

Name what the gesture means. Impulse owns the mechanism.

const tap = useTap({ onTap: select })

Why Impulse

RNGH is a good library, and it is not the problem. It exposes mechanisms on purpose. Impulse clears the sharp edges that every app otherwise rebuilds.

Intents, not mechanisms

One hook per intent. RNGH exposes activation criteria, relations, and state machines and leaves the intent to you. Impulse names the intent and keeps the mechanism as the escape hatch.

Composition is data

useGestures([tap, double, pan], { mode: "race" }) reads left to right, and a composed result is itself composable. No inside-out Gesture.Simultaneous(Gesture.Race(...)) nesting.

Three named relations

alongside runs together, blocks wins over, deferTo waits for the other to fail. Each maps to exactly one RNGH relation, so coexisting with a scroll view stops being a guess.

Stable gesture identity

A JS-thread callback is never a gesture dependency, so an inline callback cannot re-attach the detector mid-drag. A test pins identity across an inline-callback re-render.

The callback names its thread

onBegin, onUpdate, and onFinalize are worklets. onTap, onDragEnd, and onLongPress run on JS, and Impulse owns the scheduleOnRN boundary. You write no runOnJS.

Payloads shaped per intent

A drag hands back { translation, velocity, absolute, settled }. The flat union of translationX, focalX, and numberOfPointers is never the public type.

The same drag, twice

A horizontal drag that must let a vertical scroll view win. In RNGH that is a builder chain, a pair of shared values, and the start.value dance. In Impulse drag.x accumulates across gestures, and deferTo is the relation.

Read useDrag
SwipeRow.tsx
import { useDrag } from '@rootnative/impulse'

export function SwipeRow() {
  const drag = useDrag({
    axis: 'x',
    deferTo: scrollRef,
    onDragEnd: (e) => commit(e.translation.x > 100),
  })

  return (
    <GestureDetector gesture={drag.gesture}>
      <Row x={drag.x} />
    </GestureDetector>
  )
}

Impulse detects. It does not animate.

Every hook returns shared values and a gesture. Impulse starts no animation, owns no transition vocabulary, and ships no components. Pass the values to the animation library you already use.

Impulse

Detects the gesture. Hands back shared values, a payload shaped for the intent, and the gesture itself.

Inertia

Animates. @rootnative/inertia is the other half of the pair. Impulse will never require an animation library.

Ready to detect?

Pick the intent, hand it a callback, and wrap the view in a GestureDetector.