Gesture Handling
Implement touch gestures with React Native Gesture Handler for swipe, pan, pinch, and long press interactions
When to Use
- Adding swipe-to-delete or swipe-to-reveal actions
- Building draggable cards, bottom sheets, or reorderable lists
- Implementing pinch-to-zoom on images or maps
- Creating custom navigation gestures (swipe back, pull to refresh)
- Handling simultaneous and competing gesture recognition
Instructions
- Use React Native Gesture Handler v2 (RNGH) with the declarative API. The v2 API uses
Gestureobjects composed withGestureDetector, replacing the old component-based API.
npx expo install react-native-gesture-handler react-native-reanimated
Wrap your app root with GestureHandlerRootView:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Navigation />
</GestureHandlerRootView>
);
}
- Create a pan gesture for draggable elements.
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const pan = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>Drag me</Text>
</Animated.View>
</GestureDetector>
);
}
- Implement swipe-to-delete with a horizontal pan gesture.
function SwipeableRow({ onDelete, children }: Props) {
const translateX = useSharedValue(0);
const DELETE_THRESHOLD = -100;
const pan = Gesture.Pan()
.activeOffsetX([-10, 10]) // Activate only for horizontal movement
.onUpdate((e) => {
translateX.value = Math.min(0, e.translationX); // Only swipe left
})
.onEnd(() => {
if (translateX.value < DELETE_THRESHOLD) {
translateX.value = withTiming(-300, {}, () => {
runOnJS(onDelete)();
});
} else {
translateX.value = withSpring(0);
}
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={style}>{children}</Animated.View>
</GestureDetector>
);
}
- Implement pinch-to-zoom.
function ZoomableImage({ source }: { source: ImageSourcePropType }) {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const pinch = Gesture.Pinch()
.onUpdate((e) => {
scale.value = savedScale.value * e.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
if (scale.value < 1) {
scale.value = withSpring(1);
savedScale.value = 1;
}
});
const style = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={pinch}>
<Animated.Image source={source} style={[styles.image, style]} />
</GestureDetector>
);
}
- Compose multiple gestures. Use
Gesture.Simultaneous()for gestures that should work together (pan + pinch) andGesture.Exclusive()for competing gestures (tap vs. long press).
const pan = Gesture.Pan().onUpdate(/* ... */);
const pinch = Gesture.Pinch().onUpdate(/* ... */);
// Both gestures active at the same time
const combined = Gesture.Simultaneous(pan, pinch);
// Only one gesture wins
const tap = Gesture.Tap().onEnd(handleTap);
const longPress = Gesture.LongPress().minDuration(500).onEnd(handleLongPress);
const exclusive = Gesture.Exclusive(longPress, tap); // Long press takes priority
return <GestureDetector gesture={combined}>{/* ... */}</GestureDetector>;
- Use
runOnJSto call JavaScript functions from gesture worklets. Gesture callbacks run on the UI thread. To call React state setters or navigation, wrap them withrunOnJS.
const tap = Gesture.Tap().onEnd(() => {
runOnJS(navigation.navigate)('Details');
});
- Set activation constraints to prevent accidental activation.
const pan = Gesture.Pan()
.minDistance(10) // Minimum pixels before activation
.activeOffsetX([-20, 20]) // Only activate for horizontal movement
.failOffsetY([-5, 5]); // Fail if vertical movement exceeds threshold
Details
RNGH v2 vs. v1: The v2 declarative API (Gesture.Pan()) replaces v1's component API (<PanGestureHandler>). v2 is composable, works directly with Reanimated worklets, and supports gesture composition natively.
UI thread execution: RNGH gesture callbacks and Reanimated worklets run on the native UI thread, not the JavaScript thread. This means gestures remain responsive even if JavaScript is busy. Always use shared values (useSharedValue) instead of React state for gesture-driven animations.
Gesture states: Each gesture transitions through states: UNDETERMINED -> BEGAN -> ACTIVE -> END (or FAILED/CANCELLED). Use onStart (BEGAN), onUpdate (ACTIVE), and onEnd (END) to respond at each phase.
Common mistakes:
- Forgetting
GestureHandlerRootViewat the app root (gestures silently fail) - Using React state instead of shared values in gesture callbacks (drops frames)
- Not setting activation offsets (pan interferes with scroll)
- Calling JavaScript functions directly in worklets without
runOnJS
Source
https://docs.swmansion.com/react-native-gesture-handler/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.