Skip to main content

Gestures

React Native has a built-in JS touch system, the Gesture Responder System: on each touch, views are asked whether they want to become the responder, and only one view at a time receives the touch. Pressable is built on it for taps and long presses, PanResponder for drags. For anything beyond that, apps use React Native Gesture Handler (RNGH): gestures are recognized by the platform's native touch system, can run on the UI thread together with Reanimated, and can be composed (pinch and rotate at once, tap vs double tap).

Applies to: React Native 0.88, Gesture Handler 3, Reanimated 4.

Details​

Gesture Responder System​

CallbackWhen
onStartShouldSetResponder, onMoveShouldSetResponderA touch starts or moves: return true to ask to become the responder
onResponderGrantThe view became the responder: show feedback
onResponderMoveThe finger moves
onResponderReleaseThe finger is lifted
onResponderTerminationRequestAnother view asks for the responder: return false to keep it
onResponderTerminateThe responder was taken away (by a parent ScrollView, the OS)

Who becomes the responder:

  • on*ShouldSetResponder is asked bubbling from the deepest view up, so the innermost interested view (a button) wins by default.
  • on*ShouldSetResponderCapture is asked first, from the root down, so a parent can take the touch before its children; for example a container that must not let a button react while it is being swiped.

Built-In Tools​

ToolUse for
PressableTaps and long presses; style by pressed state, hitSlop. Replaces the older Touchable* components
PanResponderA drag in plain JS: PanResponder.create({...}) wraps the responder callbacks and adds gestureState (dx, dy, vx, vy, numberActiveTouches)

The responder system runs in JS: a busy JS thread delays the reaction to a touch, and native views (scroll views, maps) compete with JS responders.

React Native Gesture Handler​

ConceptWhat it is
Gesture hooksusePanGesture, useTapGesture, usePinchGesture, useRotationGesture, useLongPressGesture, useFlingGesture; config and callbacks in one object
GestureDetectorAttaches a gesture to its child view
CallbacksonBegin → onActivate → onUpdate → onDeactivate → onFinalize
UI threadWith Reanimated installed, callbacks are worklets and update shared values on the UI thread without JS
GestureHandlerRootViewMust wrap the app (and the content of every Modal on Android); gestures outside it are not recognized

Composition of gestures on one view:

HookBehaviorExample
useSimultaneousGesturesAll can be active at the same timePan + pinch + rotate a photo
useCompetingGesturesThe first to activate cancels the othersPan vs long press
useExclusiveGesturesPriority by order: a gesture activates only after the earlier ones failDouble tap before single tap

For gestures on different views: simultaneousWith, requireToFail and block in the gesture config.

Gesture Handler 2 had a builder API that is still common in existing code and deprecated in version 3: Gesture.Pan().onUpdate(...), composed with Gesture.Simultaneous, Gesture.Race and Gesture.Exclusive; onStart / onEnd are now onActivate / onDeactivate. The even older handler components (PanGestureHandler) are deprecated, and Reanimated's useAnimatedGestureHandler, used with them, is removed in Reanimated 4.

Example​

A draggable box that springs back when released:

import { StyleSheet } from 'react-native';
import {
GestureDetector,
GestureHandlerRootView,
usePanGesture,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';

export default function App() {
const x = useSharedValue(0);
const y = useSharedValue(0);

const pan = usePanGesture({
onUpdate: (event) => {
x.value = event.translationX;
y.value = event.translationY;
},
onDeactivate: () => {
x.value = withSpring(0);
y.value = withSpring(0);
},
});

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }, { translateY: y.value }],
}));

return (
<GestureHandlerRootView style={styles.root}>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
root: { flex: 1, alignItems: 'center', justifyContent: 'center' },
box: { width: 100, height: 100, backgroundColor: 'royalblue' },
});
PanResponder example
import { useRef } from 'react';
import { Animated, PanResponder } from 'react-native';

export function Draggable() {
const position = useRef(new Animated.ValueXY()).current;

const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: position.x, dy: position.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
Animated.spring(position, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
},
})
).current;

return (
<Animated.View
{...panResponder.panHandlers}
style={[{ width: 100, height: 100 }, position.getLayout()]}
/>
);
}

Every move event goes through the JS thread, so the drag lags when JS is busy.

Follow-up Questions​

  • How do you allow a horizontal swipe inside a vertical ScrollView? activeOffsetX and failOffsetY, so the pan activates only on a clear horizontal move.
  • What is hitSlop? On Pressable it extends the touch area without changing the layout, but not past the parent's bounds; in Gesture Handler it mainly shrinks it: negative values on both platforms, positive only on Android. For the minimum target size see Accessibility.
  • Why does React Native still have the responder system? Core components such as ScrollView and Pressable use it; Gesture Handler is a third-party library.

Common Mistakes​

  • Forgetting GestureHandlerRootView: GestureDetector throws in development, and in production the gestures are not recognized.
  • Using TouchableOpacity in new code instead of Pressable.
  • Updating React state on every onUpdate: it re-renders on every frame; update shared values instead.

See Also​