Use Gesture Handler and Reanimated in a One native route
One native routes run the standard animation stack: React Native Gesture Handler 3 for gestures, Reanimated 4 for UI-thread animation, and Worklets for the runtime underneath. Gesture callbacks that update shared values run on the UI runtime; One’s native compiler applies the Worklets transform automatically, so no Babel plugin configuration is needed. One exposes no alternate gesture or animation facade: these remain independent package APIs.
Add all three packages, then rebuild the native app so their native modules link:
bun add react-native-gesture-handler@~3.3.0 react-native-reanimated@~4.7.1 react-native-worklets@~0.13.0bun run prebuild:nativebun run iosimport { useState } from 'react'import { Text } from 'react-native'import { GestureDetector, GestureHandlerRootView, usePanGesture,} from 'react-native-gesture-handler'import Animated, { useAnimatedStyle, useSharedValue, withTiming,} from 'react-native-reanimated'import { scheduleOnRN } from 'react-native-worklets'
export default function DragRoute() { const [distance, setDistance] = useState(0) const x = useSharedValue(0) const style = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] })) const pan = usePanGesture({ onUpdate: (event) => { x.value = event.translationX }, onDeactivate: (event) => { scheduleOnRN(setDistance, Math.round(event.translationX)) x.value = withTiming(120) }, })
return ( <GestureHandlerRootView style={{ flex: 1, padding: 24 }}> <Text>Dragged {distance} points</Text> <GestureDetector gesture={pan}> <Animated.View style={[{ width: 72, height: 72, backgroundColor: 'blue' }, style]} /> </GestureDetector> </GestureHandlerRootView> )}The pan writes a shared value on the UI runtime every frame, animates
back on release, and reports the final distance to React state through
scheduleOnRN. The root view wraps the gesture area.
Wrap the gesture area in GestureHandlerRootView. A route-local wrapper
covers gestures that stay in the route; content presented in a separate
native sheet needs its own wrapper, since the handler tree ends at the
presentation boundary. One does not install or wrap Gesture Handler for
every app automatically.
Gesture callbacks run on the UI runtime, where shared values live. Reads
and writes there never cross to JavaScript, which is why the drag stays
at frame rate under JS load. When a callback must update React state,
scheduleOnRN hops the call to the React Native runtime with its
arguments. Animated styles subscribe to shared values and recompute on
the UI side; withTiming drives the value back on a curve without
further callbacks.
One.iOS views also take SwiftUI’s own gestures through swiftStyle, with
gesture, highPriorityGesture and simultaneousGesture. Each takes a
kind (drag, tap, spatialTap, longPress, magnify, rotate), the
gesture’s initializer options, and callbacks:
<One.iOS.Text text="Drag me" swiftStyle={{ gesture: { kind: 'drag', minimumDistance: 4, onChanged: ({ translation }) => setDragging(translation.height > 0), onEnded: ({ translation, velocity, predictedEndTranslation }) => settle(predictedEndTranslation.height), }, }}/>onEnded receives the gesture’s value: for a drag, its location,
translation, velocity and predicted end. onChanged is available on
every kind except tap, and native only reports changes when you pass it.
Each change crosses to JavaScript, so use it for state and haptics. To move
a view with the finger, bind the drag to native state instead.
A drag can write a useNativeState number itself, so the view follows the
finger without waiting for JavaScript. Continuous number fields in
swiftStyle (CGFloat or Double in SwiftUI, such as width, height,
offset and the frame sizes) accept the same handle and read it natively.
const offset = useNativeState(0)
<One.iOS.Rectangle style={{ height: 700 }} swiftStyle={{ offset: { x: 0, y: offset }, gesture: { kind: 'drag', bind: { state: offset, axis: 'y', range: [0, 600] }, snap: { targets: [0, 300, 600], animation: { kind: 'spring', response: 0.35, dampingFraction: 0.8 }, }, onEnded: ({ snapTarget }) => setDetent(snapTarget), }, }}/>bind writes the state’s value at the start of the drag plus the
translation along axis, clamped to range.snap takes absolute targets in the same units as the state. On release,
native code picks the target nearest the predicted end, animates to it,
and reports it to onEnded as snapTarget.style.| Packages | Type | Description |
|---|---|---|
react-native-gesture-handler~3.3.0Detectors, gestures, and the root view. | ~3.3.0 | Detectors, gestures, and the root view. |
react-native-reanimated~4.7.1Shared values, animated styles, and drivers. | ~4.7.1 | Shared values, animated styles, and drivers. |
react-native-worklets~0.13.0The UI runtime and thread hops. | ~0.13.0 | The UI runtime and thread hops. |
| iOS | Android | Web | |
|---|---|---|---|
| Gestures | Native handler | Native handler | JS fallback |
| Animation | UI runtime | UI runtime | Main thread |
| Transform | OXC plugin | OXC plugin | OXC plugin |
scheduleOnRN (or the Reanimated equivalent) when a gesture callback
must reach React state..value in render does not
subscribe; only animated styles and worklets track them.import { GestureDetector, GestureHandlerRootView, usePanGesture } from 'react-native-gesture-handler'import Animated, { useAnimatedStyle, useSharedValue, withSpring,} from 'react-native-reanimated'
function SnapSlider() { const x = useSharedValue(0) const style = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] })) const pan = usePanGesture({ onUpdate: (event) => { x.value = Math.max(0, Math.min(200, event.translationX)) }, onDeactivate: () => { x.value = withSpring(0) }, })
return ( <GestureHandlerRootView style={{ flex: 1 }}> <GestureDetector gesture={pan}> <Animated.View style={[{ width: 48, height: 48, borderRadius: 24 }, style]} /> </GestureDetector> </GestureHandlerRootView> )}Clamping in the update handler bounds the drag without any React involvement, and the spring returns the knob on release. The whole interaction runs on the UI runtime.
import { useState } from 'react'import { Text } from 'react-native'import { GestureDetector, GestureHandlerRootView, usePanGesture } from 'react-native-gesture-handler'import { scheduleOnRN } from 'react-native-worklets'
function SwipeCounter() { const [swipes, setSwipes] = useState(0) const pan = usePanGesture({ onDeactivate: (event) => { if (Math.abs(event.translationX) > 80) { scheduleOnRN(setSwipes, swipes + 1) } }, })
return ( <GestureHandlerRootView style={{ flex: 1 }}> <GestureDetector gesture={pan}> <Text>Swipes: {swipes}</Text> </GestureDetector> </GestureHandlerRootView> )}The threshold check runs on the UI side so short drags never wake React. Only a committed swipe hops over to bump the counter.
Edit this page on GitHub.