React Native Gesture Handler 2 in Expo: Pan, Pinch, Swipe, and Long Press in 2026
Add pan, pinch, swipe, and long-press gestures to your React Native app with Gesture Handler 2 and Expo SDK 55. Reanimated worklets and code samples included.
React Native Gesture Handler 2 is the official, native-backed gesture library for React Native and Expo. It replaces the legacy PanResponder with a declarative Gesture API that runs on the UI thread, plugs into Reanimated 4 worklets, and works under the New Architecture (Fabric + TurboModules). In 2026 it ships with Expo SDK 55 out of the box, supports stylus and pointer events on tablets, and powers nearly every modern gesture interaction (swipeable rows, pinch-to-zoom, draggable bottom sheets, custom carousels) without ever crossing the JavaScript bridge.
Honestly, after shipping three apps with the old PanResponder, switching to Gesture Handler 2 felt like cheating. The drags just don't drop frames anymore.
Install with npx expo install react-native-gesture-handler and wrap your root with <GestureHandlerRootView style={{ flex: 1 }}>. It's required, not optional.
Use the new Gesture API (Gesture.Pan(), Gesture.Pinch(), Gesture.Tap(), Gesture.LongPress(), Gesture.Fling()) instead of legacy PanGestureHandler components.
Compose gestures with Gesture.Simultaneous(), Gesture.Race(), and Gesture.Exclusive() for pan-and-pinch, swipe-to-dismiss, or tap-vs-long-press flows.
Read and write useSharedValue from gesture callbacks directly. Handlers run as worklets on the UI thread, so 60 to 120fps is the default.
Gesture Handler 2 is fully compatible with the React Native New Architecture and integrates cleanly with libraries like @gorhom/bottom-sheet and react-native-reanimated-carousel.
Install Gesture Handler 2 in Expo
In a managed Expo project, install the library through the Expo CLI so the correct version for your SDK is resolved automatically. As of Expo SDK 55, the pinned version is react-native-gesture-handler@~2.20.x. Run:
Then wrap your root layout (for Expo Router projects, that's app/_layout.tsx) with GestureHandlerRootView. Without it, gestures will silently fail on Android and inside iOS modals:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack />
</GestureHandlerRootView>
);
}
If you use a custom Babel config, make sure react-native-worklets/plugin is the last entry in plugins. Reanimated 4 split worklets into a separate package, and Gesture Handler depends on it. Rebuild with npx expo prebuild --clean if you previously had Reanimated 3 configured, then run npx expo run:ios or npx expo run:android to refresh the native side. Expo Go ships the library prebuilt, so no native rebuild is required if you stay in Expo Go.
Why Gesture Handler instead of PanResponder?
React Native's built-in PanResponder runs on the JavaScript thread. Every frame of a drag has to ferry touch events from native to JS, run your callback, and ferry the new style back. Under load, this drops frames to 20–30fps and creates the rubber-banding many older apps suffer from.
Gesture Handler moves the recognizer into native code (a real UIGestureRecognizer on iOS, GestureDetector on Android) and lets you write the response handler as a Reanimated worklet that executes on the UI thread.
The practical result: a 120Hz ProMotion iPad can drag a 4K image at full refresh, gestures keep responding while JS is blocked (image decoding, navigation transitions), and you get correct hit testing with native scroll views. Pinch a photo inside a ScrollView and the scroll view defers to your pinch automatically. For a deeper performance baseline, see our React Native performance optimization guide, which benchmarks bridge-driven vs worklet-driven animations side by side.
Build a draggable card with Gesture.Pan()
The hello-world of Gesture Handler 2 is a draggable view. The pattern: create two useSharedValues for the translation, build a Gesture.Pan() that updates them inside .onChange(), snap back in .onEnd(), and apply the values with useAnimatedStyle.
Two details I see missed in most tutorials. First, use event.changeX / changeY (delta since last frame) instead of translationX (total since gesture start) when you want to accumulate offsets across multiple drags. Second, always call .minDistance() when the draggable lives inside a ScrollView. Without it, every tap will eat your scroll.
Pinch-to-zoom with Gesture.Pinch()
Pinch follows the same pattern but exposes event.scale, the absolute scale factor since the gesture began. Multiply with a saved base scale to make pinches cumulative:
If you need the pinch focal point (useful for zooming a photo centered on the user's fingers rather than the view origin), read e.focalX and e.focalY and translate by the delta. The official Pinch documentation includes a worked focal-point example you can adapt directly.
Tap, double-tap, and long press
Tap and long press look trivial, but the chaining rules matter. A single tap that must wait for a possible double tap needs Gesture.Exclusive(double, single):
Because gesture callbacks run on the UI thread, calling React state setters directly throws. Wrap them with runOnJS. Use runOnJS sparingly inside .onChange(), though. Cross-thread calls have a small cost, and at 120Hz the noise adds up. Prefer shared values for visual state, and only hop back to JS at gesture end.
Compose pan, pinch, and rotation simultaneously
Real apps rarely have a single gesture in isolation. A photo viewer needs pan, pinch, and rotation acting together. A swipeable card may need to scroll a list and allow horizontal swipe. Gesture Handler 2 gives you three composers:
Gesture.Simultaneous(a, b): both gestures can be active at once. Use this for pan + pinch + rotate.
Gesture.Race(a, b): the first to be recognized wins and cancels the others. Use this for swipe-vs-tap.
Gesture.Exclusive(a, b): gestures are tried in order; later ones only fire if earlier ones fail. Use this for single-tap-after-double-tap.
For pan-inside-scrollview, use .activeOffsetX([-10, 10]) on the inner pan. It tells the recognizer to wait until horizontal movement passes 10 pixels before activating, so vertical scrolls pass through to the parent ScrollView untouched.
Swipe-to-delete rows with ReanimatedSwipeable
Gesture Handler 2 ships a high-level ReanimatedSwipeable component that handles the swipe-row pattern correctly: rubber-banding past the action width, snap-open thresholds, and programmatic close. Pair it with our FlashList v2 list for a high-performance swipeable list:
If you're building a draggable sheet, please don't roll your own. Reach for the dedicated @gorhom/bottom-sheet with Gesture Handler. It handles keyboard avoidance, accessibility, and snap-point physics for you, and I've lost too many evenings reimplementing that physics by hand.
New Architecture and platform notes
Gesture Handler 2.16 and later fully support the React Native New Architecture (Bridgeless mode, Fabric renderer, TurboModules). Expo SDK 55 enables New Architecture by default for new projects. Three platform-specific behaviors are worth knowing:
iOS modals: gestures inside an iOS modal need their own GestureHandlerRootView wrapper because modals are presented in a separate UIWindow. Wrap the modal content, not just the app root.
Android touch slop: Android's default touch slop is larger than iOS, so pan gestures can feel "lazy." Override with .activeOffsetX(5) or set .minDistance(2) for parity.
Web: react-native-web partially supports Gesture Handler via pointer events. Pan, tap, and long press work; pinch and rotation require touch hardware. Test in a real browser on a touch device, not Chrome DevTools' touch emulator.
For release-channel migrations and other 2026 breaking changes, see the Gesture Handler GitHub release notes and the project's official migration guide.
Why does my gesture not work?
Four issues account for the vast majority of "my pan doesn't fire" bug reports. I've hit each of these in production at least once, so the order roughly matches frequency:
Missing GestureHandlerRootView: the most common cause. Without it, touches never reach the recognizer on Android. Wrap your root layout, every modal, and every standalone screen presented outside the navigator.
State setter called from a worklet: if you do setSomething(value) inside .onChange(), you get a runtime error. Wrap with runOnJS(setSomething)(value), or push to a shared value and read it from a useAnimatedReaction.
Gesture inside a parent Pressable: the pressable steals touches. Replace it with a View and add a Gesture.Tap(), then compose with your pan via Gesture.Exclusive.
Reanimated plugin not in Babel: gestures appear to fire but shared values never update, and you see "Reanimated 4 failed to create a worklet" in the console. Confirm react-native-worklets/plugin is the last plugin and rebuild.
Frequently Asked Questions
What is the difference between PanResponder and Gesture Handler?
PanResponder is React Native's built-in gesture system, and it runs entirely on the JavaScript thread, so it drops frames under load. Gesture Handler 2 ships native recognizers on iOS and Android and runs callbacks as Reanimated worklets on the UI thread, giving you consistent 60 to 120fps interactions even while JS is busy.
Do I need GestureHandlerRootView in Expo?
Yes, it's required, not optional. Without it, gestures will not receive touch events on Android, and they will fail inside iOS modals. Wrap your app root (for Expo Router, app/_layout.tsx) and any modal content separately.
Can I use Gesture Handler without Reanimated?
Technically yes. You can call setState from gesture callbacks via runOnJS, but you give up the entire performance benefit. In practice every modern tutorial and the official docs assume Reanimated, and Gesture Handler 2's API is designed to feed shared values directly.
Does react-native-gesture-handler work with the New Architecture?
Yes. Versions 2.16 and later are fully compatible with Fabric, TurboModules, and Bridgeless mode. Expo SDK 55 enables the New Architecture by default and ships a compatible Gesture Handler build out of the box.
Why does my Pan gesture not activate inside a ScrollView?
The parent ScrollView is winning the gesture race. Add .activeOffsetX([-10, 10]) (or the Y variant for vertical pans) to your Gesture.Pan() so the recognizer waits for enough movement in your direction before activating, letting orthogonal scrolls pass through.
Sofia is a mobile platform engineer with seven years of React Native experience, focused on developer tooling and CI/CD for mobile teams. She spent three years at Shopify on the Point of Sale app, where she helped move the team off Bitrise onto a custom EAS Build setup and wrote much of the internal testing harness for Detox flake reduction.
Before Shopify she was at a Berlin-based scooter startup where she was the second mobile hire and shipped the first RN version of the rider app to roughly 40 cities. She runs a small consultancy now, mostly helping Series A and B startups untangle their Fastlane lanes and set up over-the-air update strategies that don't violate App Store guidelines. Her writing leans toward the unglamorous middle of the stack: provisioning profiles, monorepo setups with Nx, and why your bundle size keeps creeping up.
A practical walkthrough of Reanimated 4's worklets split on Expo SDK 53. Install react-native-worklets, fix the babel plugin path, migrate runOnUI calls, and avoid the silent runtime crash.
A field-tested look at the four real CodePush alternatives for React Native in 2026: Expo Updates, App.fly.io, Microsoft Hot Update, and self-hosted, with the migration costs I actually paid.
I've migrated two production React Native apps to the New Architecture (Fabric + TurboModules + Codegen) in the last few months. The official upgrade guide is fine for fresh apps, but for anything older.