Gestures / Reanimated
react-native-gesture-handler runs gesture recognition on the native UI thread, giving you 60fps interaction even when the JS thread is busy. Pair it with react-native-reanimated for shared values and worklets so animations stay native too. Use it for drawer swipes, pull-to-refresh, swipe-to-delete, and any custom gesture.
Pan, pinch, long press, and swipe-to-delete
EXAMPLE
// npm i react-native-gesture-handler react-native-reanimated
// Wrap your app root in <GestureHandlerRootView>.
import React from 'react';
import { GestureHandlerRootView, GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue, useAnimatedStyle, withSpring, withTiming, runOnJS,
} from 'react-native-reanimated';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
const { width } = Dimensions.get('window');
// 1) Pan — drag a card around the screen
export function DraggableCard() {
const tx = useSharedValue(0);
const ty = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((e) => {
tx.value += e.changeX;
ty.value += e.changeY;
})
.onEnd(() => {
tx.value = withSpring(0);
ty.value = withSpring(0);
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: tx.value }, { translateY: ty.value }],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, style]}><Text>Drag me</Text></Animated.View>
</GestureDetector>
);
}
// 2) Pinch + rotate — composed gestures
export function PinchableImage({ source }) {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const pinch = Gesture.Pinch().onChange((e) => { scale.value *= 1 + (e.scaleChange - 1); });
const rotate = Gesture.Rotation().onChange((e) => { rotation.value += e.rotationChange; });
const both = Gesture.Simultaneous(pinch, rotate);
const style = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }, { rotateZ: \`${rotation.value}rad\` }],
}));
return (
<GestureDetector gesture={both}>
<Animated.Image source={source} style={[styles.image, style]} />
</GestureDetector>
);
}
// 3) Swipe-to-delete row
export function SwipeRow({ label, onDelete }) {
const tx = useSharedValue(0);
const swipe = Gesture.Pan()
.activeOffsetX([-10, 10])
.onChange((e) => { tx.value = Math.min(0, tx.value + e.changeX); })
.onEnd(() => {
if (tx.value < -width * 0.4) {
tx.value = withTiming(-width, { duration: 180 }, (done) => {
if (done) runOnJS(onDelete)();
});
} else {
tx.value = withSpring(0);
}
});
const style = useAnimatedStyle(() => ({ transform: [{ translateX: tx.value }] }));
return (
<GestureDetector gesture={swipe}>
<Animated.View style={[styles.row, style]}>
<Text>{label}</Text>
</Animated.View>
</GestureDetector>
);
}
// 4) Long press to enter selection mode
export function LongPressable({ onSelect, children }) {
const longPress = Gesture.LongPress()
.minDuration(400)
.onStart(() => runOnJS(onSelect)());
return <GestureDetector gesture={longPress}>{children}</GestureDetector>;
}
const styles = StyleSheet.create({
card: { width: 180, height: 100, backgroundColor: '#dbeafe', borderRadius: 12,
alignItems: 'center', justifyContent: 'center' },
image: { width: 240, height: 240, alignSelf: 'center' },
row: { padding: 16, backgroundColor: 'white', borderBottomWidth: 1, borderColor: '#eee' },
});
Why it matters
Anchor the gesture root with GestureHandlerRootView at the top of your tree, otherwise gestures inside modals and overlays silently fail. The rule of thumb: every screen that uses gestures should be a child of one root, never multiple.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
const tap = Gesture.Tap().onEnd(() => console.log('tapped'));
<GestureDetector gesture={tap}>
<View style={{ padding: 32 }}><Text>Tap me</Text></View>
</GestureDetector>
Try it Yourself »
Discussion
Loading…