Skip to content
rootnativePublic

About

Wrapper around react native gesture handler library to make the gestures easier

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

58 Commits

Folders and files

Repository files navigation

Impulse logo

Impulse

Declarative gestures for React Native, on iOS, Android, and the web.
A gesture is an intent, such as useSwipe or usePinch, not a builder chain.

Build and test status Docs and demo deploy status npm version of @rootnative/impulse Expo SDK 58 Gesture Handler 3.3 Platforms: iOS, Android, and web (partial) MIT licence

Open the live demo Β· Try it Β· Quick start Β· Documentation Β· LLM docs

The Impulse docs home page, with the headline 'Declarative gestures for React Native.' and the example gallery in a phone frame

Status: 0.0.0-alpha.2, an alpha. Eight intent hooks ship, with composition and coexistence. useHover and useEdgeSwipe are not written yet. Every intent was checked on an iOS simulator and an Android emulator. Nobody has tested the feel of a gesture yet.

✨ Features

Feature What it does
Intents, not mechanisms One hook for each intent: tap, double tap, long press, drag, pan, swipe, pinch, and rotate. You don't set activation criteria by hand.
Composition as data useGestures([tap, doubleTap, pan], { mode: 'race' }) reads from left to right. A composed result can compose again.
Three coexistence options Each hook takes alongside, blocks, and deferTo. Each option maps to exactly one Gesture Handler relation.
Stable gesture identity An inline callback never rebuilds the gesture, so <GestureDetector> doesn't attach it again in the middle of a drag. A test pins this.
The callback name states its thread onBegin, onUpdate, and onFinalize are worklets. onTap, onSwipe, and onDragEnd run on the JS thread, and Impulse owns the boundary.
Payloads for each intent A drag reports { translation, velocity, absolute, settled }, not the flat fields of Gesture Handler.
Shared values out, no animation in Each hook returns shared values and a gesture. You animate them with the library that you already use.
Accessibility, stated The docs page of each intent names its fallback for a screen reader and a keyboard.
Web behaviour, stated The docs say what works in a browser, and what nobody has checked.
Escape hatches useRawGesture gives you the mechanism. @rootnative/impulse/gesture-handler re-exports Gesture Handler, so Impulse stays your only gesture import.
One subpath for each hook @rootnative/impulse/drag ships only useDrag. The package has no side effects at import.
A Jest preset @rootnative/impulse/jest-preset sets up the mocks in one line.

πŸ–οΈ The intent hooks

Hook What it reports Web
useTap A single tap, and isActive for the pressed state Works under a mouse
useDoubleTap Two taps, paired with a single tap Works under a mouse
useLongPress A press held past a duration, while the finger is down Works under a mouse
useDrag An x and a y that follow the finger, with bounds and elastic Works under a mouse
usePan The movement of each frame. Your screen owns the position. Not verified
useSwipe A pan judged at release: which way, and how hard Not verified
usePinch A two-finger pinch that owns its scale Not verified
useRotate A two-finger rotation that owns its angle, in degrees Not verified
useHover, useEdgeSwipe Designed, not written β€”

πŸ“± Try it

The useDoubleTap screen of the demo: a card that takes one tap or two, a maxDelay choice, and the tap counts Β  The useSwipe screen of the demo: horizontal cards to swipe inside a vertical list Β  The usePinch screen of the demo: a zoom viewer with its scale shown as 1.00x

In a browser On a phone
rootnative.github.io/impulse/example

One screen for each intent.
You don't need an install or an account.
Clone this repository, then run
pnpm install and pnpm run example.

Scan the QR code in the terminal with Expo Go for SDK 58.

Note

A browser has a mouse, not fingers. Only useTap, useDoubleTap, useLongPress, and useDrag were checked in a desktop browser. Use a phone to try the pinch, the rotation, and the swipe.

πŸš€ Quick start

npx expo install @rootnative/impulse react-native-gesture-handler react-native-reanimated react-native-worklets

npx expo install picks the peer versions that match your Expo SDK. Expo adds the worklets Babel plugin for you. In a bare React Native app, add react-native-worklets/plugin as the last Babel plugin. Then run npx pod-install for iOS.

Render <GestureHandlerRootView> above every component that uses a gesture:

import { GestureHandlerRootView } from 'react-native-gesture-handler'

export default function App() {
  return (
    <GestureHandlerRootView style={styles.root}>
      <Navigation />
    </GestureHandlerRootView>
  )
}

const styles = StyleSheet.create({
  root: { flex: 1 },
})

Warning

Without the root view, a gesture never fires. In development on iOS and Android, Gesture Handler 3 throws an error that names the root view. On web and in a release build, nothing throws and nothing warns. If a gesture does nothing at all, check the root view first.

Then attach a gesture:

import { GestureDetector, useTap } from '@rootnative/impulse'

function Card() {
  const tap = useTap({ onTap: () => select() })

  return (
    <GestureDetector gesture={tap.gesture}>
      <View style={styles.card} />
    </GestureDetector>
  )
}

Tip

Import one hook from its subpath, such as @rootnative/impulse/drag. Then an app that uses one gesture doesn't ship the set.

πŸ’‘ Why Impulse

Gesture Handler is a good library. It gives you mechanisms: activation criteria, gesture relations, and state machines. It leaves each intent to you. So each app builds swipe to dismiss, double tap, and pinch from Pan and its own maths again. Each copy gets the traps wrong in its own way.

The same horizontal drag inside a vertical scroll view, twice:

// Gesture Handler
const x = useSharedValue(0)
const start = useSharedValue(0)

const pan = useMemo(
  () =>
    Gesture.Pan()
      .activeOffsetX([-10, 10])
      .failOffsetY([-5, 5])
      .requireExternalGestureToFail(scrollRef)
      .onStart(() => {
        start.value = x.value
      })
      .onUpdate((e) => {
        x.value = start.value + e.translationX
      })
      .onEnd((e) => {
        runOnJS(commit)(e.translationX > 100)
      }),
  [],
)
// Impulse
const drag = useDrag({
  axis: 'x',
  deferTo: scrollRef,
  onDragEnd: (e) => commit(e.translation.x > 100),
})

drag.x is a shared value that keeps its position across gestures, so the start.value step is gone. deferTo names the relation by what it does. The step to the JS thread is the job of Impulse, not yours.

🧭 How it works

flowchart LR
  subgraph app ["Your component"]
    direction LR
    hook["Intent hook<br/>useDrag, useSwipe, ..."] --> detector["GestureDetector"]
  end
  hook -->|builds once| rngh["Gesture Handler<br/>activation and relations"]
  rngh -->|phase worklets| values[("Shared values<br/>x, y, scale, angle")]
  rngh -->|scheduleOnRN| intent["Intent callbacks<br/>onTap, onSwipe, onDragEnd"]
  values --> anim["Your animation code"]
Loading
  • The gesture is built once. An inline callback goes through a ref, so a new render doesn't make a new gesture.
  • The callback name states its thread. A phase callback runs on the UI thread. An intent callback runs on the JS thread, and Impulse crosses the boundary for you.
  • Impulse detects. It doesn't animate. It ships no springs, no transitions, and no components. A bridge to @rootnative/inertia is planned as an optional subpath, and Impulse will never require an animation library.

Impulse or @rootnative/inertia-gestures?

Both build on Gesture Handler, and both ship a useDrag, a usePan, and a useSwipe. The names are the same, but the products are different. @rootnative/inertia-gestures requires @rootnative/inertia and gives back an animatedStyle that already knows the bounds and the spring back. Impulse requires no animation library and gives back the gesture and its values. Use -gestures to make a Motion.* component follow a finger. Use Impulse for the gesture itself.

πŸ“… Runtime

Impulse is developed against Expo SDK 58: React 19.3.0, React Native 0.88.1, Gesture Handler 3.3.0, Reanimated 4.7.1, and Worklets 0.13.0. An Expo SDK 57 app uses 0.0.0-alpha.1.

Peer Range
react >=19.3.0 <20.0.0
react-native >=0.88.0 <0.89.0
react-native-gesture-handler >=3.3.0 <4.0.0
react-native-reanimated >=4.7.0 <4.8.0
react-native-worklets >=0.13.0 <0.14.0

The Reanimated and Worklets ranges are narrow on purpose. The two packages are locked to each other, and a wider range lets a package manager pair versions that don't install.

Impulse builds its gestures with the builder API of Gesture Handler (Gesture.Pan()). Gesture Handler 3 marks that API as deprecated, and it still runs it. A gesture from the Gesture Handler 3 hook API (usePanGesture) doesn't compose with an Impulse gesture.

πŸ—ΊοΈ Roadmap

Milestone State
1. Composition and coexistence Done. The gate (a horizontal useDrag in a vertical ScrollView, and a tap with a double tap) passed on Android and iOS. The feel is not tested.
2. The intent set Eight of ten intents ship. useHover and useEdgeSwipe are designed, not written.
3. The Inertia bridge Not started. @rootnative/impulse/inertia will turn a release payload into an Inertia release animation.

Each milestone gate needs a pass on real hardware. A test runner can't tell you if a horizontal drag inside a vertical ScrollView feels right. Read the roadmap for the detail.

πŸ€– Docs for AI agents

The package ships an llms.txt file with the exact API of its version: node_modules/@rootnative/impulse/llms.txt. The docs site serves llms.txt, an overview, and llms-full.txt, the full API reference. The AI agents page explains the two files.

πŸ“š Documentation

Page Read it to
Introduction Learn what Impulse does differently from raw Gesture Handler
Installation Install the peers, add the root view, and see the status of each platform
Composition Combine gestures with useGestures in race, simultaneous, or exclusive mode
Coexistence Choose between alongside, blocks, and deferTo next to a scroll view
Threads and callbacks Learn which callbacks are worklets and which run on the JS thread
Raw gestures Use useRawGesture when no intent fits
Gesture Handler interop Use a Gesture Handler primitive through @rootnative/impulse/gesture-handler
Web behaviour See what each intent does in a browser, and what nobody has checked
Testing Test gestures in Jest with the preset
Roadmap See each milestone and its gate

🧰 Tech stack

Layer Technology
Runtime React 19.3, React Native 0.88, Expo SDK 58
Gestures react-native-gesture-handler 3.3 (the builder API), with Reanimated 4.7 and Worklets 0.13 as peers
Language TypeScript 5, strict mode
Build tsup (ESM output), Turborepo
Package manager pnpm 10
Tests Jest 29, @testing-library/react-native, and the Gesture Handler Jest utilities
Docs Docusaurus 3
Delivery GitHub Actions, GitHub Pages, and npm with trusted publishing

πŸ”§ Development

You need Node.js 24 (the version in .nvmrc) and pnpm 10.

git clone https://github.com/rootnative/impulse.git
cd impulse
pnpm install
Command What it does
pnpm run build Builds the package with Turborepo
pnpm run dev Builds in watch mode
pnpm run typecheck Runs tsc --noEmit
pnpm run test Runs Jest
pnpm run lint Runs ESLint
pnpm run example Starts the Expo dev server of the example app
pnpm run docs:dev Starts the docs site
pnpm run build:llms Writes llms.txt and llms-full.txt again after a docs change
pnpm run check:versions Checks that each version reference agrees with the package
pnpm run preflight Runs the full gate that CI runs before a release

To run one test, name the package first:

pnpm --filter @rootnative/impulse test -- gestureIdentity

To start the example on one platform, run pnpm run example:ios, pnpm run example:android, or pnpm run example:web.

Repository layout
Path What it is
packages/core/ @rootnative/impulse, the whole public surface
docs/ The Docusaurus site. Its build also carries the web build of the example app.
example/ The Expo app, one screen for each intent. A pass on a device is a release requirement.
scripts/ build-llms.mjs, check-versions.mjs, and build-brand.mjs

Inside packages/core/src:

Path What it is
intents/ One file for each intent hook
compose/ useGestures, the only place that uses the Race, Simultaneous, and Exclusive of Gesture Handler
relations/ The resolution of alongside, blocks, and deferTo to the three relations of Gesture Handler
raw/ useRawGesture, the escape hatch
internal/ Not public. The shared memo helper, useLatestCallback, useStableList, and the dev warnings.
gesture-handler/ The re-exports of Gesture Handler, pinned to reference identity by a test

πŸ”’ Security

To report a vulnerability, read SECURITY.md.

πŸ™ Credits

πŸ“„ Licence

MIT Β© 2026 Root Native

About

Wrapper around react native gesture handler library to make the gestures easier

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages