Mobile animation gesture handler
React Native Gesture Handler - gesture types, GestureDetector, gesture composition, state machine, platform-specific gestures, swipeable rows, hover gesturesFrom its SKILL.md
npx -y skills add agents-inc/skills --skill mobile-animation-gesture-handlerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 20 stars20 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.9 KB, ~3.8k tokens by cl100k_base, as published. Nobody here has run it
React Native Gesture Handler Patterns
Quick Guide: Use Gesture Handler's v2 builder API (
Gesture.Pan(),Gesture.Tap(), etc.) withGestureDetectorfor all touch interactions. Wrap your app root inGestureHandlerRootView. Compose gestures withGesture.Simultaneous(),Gesture.Race(), andGesture.Exclusive(). UseonChange(notonUpdate) when working with animation shared values --onChangeprovides deltas (changeX),onUpdateprovides cumulative values (translationX). Gesture callbacks are automatically workletized when your animation library is installed.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST wrap the app root in GestureHandlerRootView -- gestures will silently fail without it)
(You MUST use GestureDetector with the builder API (Gesture.Pan(), etc.) -- NOT the legacy PanGestureHandler components)
(You MUST use onChange for incremental shared value updates and onUpdate for cumulative values -- mixing them causes drift)
(You MUST use Gesture.Simultaneous() for multi-touch interactions (pinch + pan) -- without it, only one gesture activates)
(You MUST NOT nest GestureDetector components using different API styles (hooks vs builder) under the same root -- this causes undefined behavior)
</critical_requirements>
Auto-detection: react-native-gesture-handler, GestureDetector, GestureHandlerRootView, Gesture.Pan, Gesture.Tap, Gesture.Pinch, Gesture.Rotation, Gesture.LongPress, Gesture.Fling, Gesture.Hover, Gesture.Simultaneous, Gesture.Race, Gesture.Exclusive, Swipeable, ReanimatedSwipeable, onBegin, onStart, onChange, onUpdate, onEnd, onFinalize
When to use:
- Adding pan, pinch, tap, rotation, long-press, fling, or hover gestures to React Native views
- Composing multiple gestures on the same view (pinch-to-zoom + drag)
- Building swipeable list rows with reveal actions
- Replacing React Native's built-in Gesture Responder System (PanResponder)
- Implementing gesture-driven animations with shared values
- Adding hover interactions for iPad trackpad, desktop, or web targets
Key patterns covered:
- GestureDetector + builder API for all gesture types
- Gesture composition: Simultaneous, Race, Exclusive
- Gesture state machine and lifecycle callbacks
- Shared value integration in gesture callbacks (onChange vs onUpdate)
- ReanimatedSwipeable for swipeable list rows
- Hover gesture for pointer devices (iPad trackpad, mouse, stylus)
- Platform-specific gesture configuration (Android ripple, iOS haptics)
- Cross-component gesture relations (requireToFail, simultaneousWith, block)
When NOT to use:
- Simple button taps (use
PressableorTouchableOpacityfrom React Native core) - Scroll-only interactions (use
ScrollVieworFlatListdirectly) - Web-only applications without React Native
Detailed Resources:
- examples/core.md - GestureDetector setup, pan, tap, pinch, rotation gestures with shared values
- examples/composition.md - Simultaneous, Race, Exclusive composition, cross-component relations
- examples/swipeable.md - ReanimatedSwipeable rows, FlatList integration, action panels
- examples/advanced.md - Hover gesture, manual gesture control, platform-specific config
- reference.md - Decision frameworks, gesture type reference, state machine diagram
<philosophy>
Philosophy
React Native Gesture Handler replaces the built-in Gesture Responder System with native-driven gesture recognition. The key advantage is that gestures are processed on the native thread, not JS -- so they remain responsive even when JS is busy.
Core principles:
- Native-first -- Gesture recognition runs natively; callbacks optionally run as worklets on the UI thread
- Declarative composition -- Define gestures as objects, compose them with
Simultaneous,Race,Exclusive - State machine driven -- Every gesture follows UNDETERMINED -> BEGAN -> ACTIVE -> END/FAILED/CANCELLED
- Builder API -- Chain configuration methods:
Gesture.Pan().minDistance(10).onUpdate(handler) - One GestureDetector per gesture (or composed gesture) -- Don't attach multiple unrelated gestures via separate nested detectors
Mental model: Think of each gesture as a state machine that competes with other gestures for activation. Composition methods (Simultaneous, Race, Exclusive) define the competition rules. The gesture that wins transitions to ACTIVE; the rest FAIL or get CANCELLED.
v3 hooks API (beta): RNGH v3 introduces a hooks-based API (usePanGesture, useSimultaneousGestures, etc.) that is cleaner but still in beta. The v2 builder API (Gesture.Pan(), GestureDetector) is the stable production API documented here.
<patterns>
Core Patterns
Pattern 1: GestureHandlerRootView Setup
Every app using Gesture Handler must wrap its root in GestureHandlerRootView. Without it, gestures silently fail -- no errors, just no recognition.
import { GestureHandlerRootView } from "react-native-gesture-handler";
export function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<AppContent />
</GestureHandlerRootView>
);
}
Why good: single root wrapper, flex: 1 ensures full-screen coverage
Gotcha (Android modals): React Native Modals on Android create a separate native view hierarchy. Wrap Modal content in its own GestureHandlerRootView -- gestures inside a Modal won't work otherwise.
Gotcha (native navigation): When using a native navigation library (separate screen containers per screen), wrap each registered screen individually rather than a single app-level root.
See examples/core.md for the full setup pattern.
Pattern 2: Pan Gesture with Shared Values
The most common pattern: drag an element by tracking translation deltas.
const offset = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((event) => {
offset.value += event.changeX; // Incremental delta
})
.onFinalize((event) => {
offset.value = withDecay({ velocity: event.velocityX });
});
// Wrap with GestureDetector + Animated.View
<GestureDetector gesture={pan}>
<Animated.View style={animatedStyle} />
</GestureDetector>
Why onChange over onUpdate: onChange provides changeX/changeY (delta since last event) -- add it to an offset. onUpdate provides translationX/translationY (cumulative since gesture start) -- use it as an absolute position. Mixing them causes position drift.
See examples/core.md for complete pan, tap, pinch, and rotation examples.
Pattern 3: Gesture Composition
Compose multiple gestures on the same view with three strategies:
const pan = Gesture.Pan().onChange(/* ... */);
const pinch = Gesture.Pinch().onUpdate(/* ... */);
const rotation = Gesture.Rotation().onUpdate(/* ... */);
// All three active simultaneously (photo viewer)
const composed = Gesture.Simultaneous(pan, pinch, rotation);
// First to activate wins, rest fail (swipe vs long-press)
const racing = Gesture.Race(pan, longPress);
// Priority order: first arg wins ties (double-tap beats single-tap)
const exclusive = Gesture.Exclusive(doubleTap, singleTap);
<GestureDetector gesture={composed}>
<Animated.View />
</GestureDetector>
Key rule: Pass the composed gesture to a single GestureDetector. Do not nest multiple GestureDetector components for gestures that should interact -- use composition instead.
See examples/composition.md for full photo viewer and tap disambiguation patterns.
Pattern 4: Gesture State Machine
Every gesture follows a state machine. Understanding it is critical for debugging gesture conflicts:
UNDETERMINED ──> BEGAN ──> ACTIVE ──> END ──> UNDETERMINED
│ │
├──> FAILED ─────────┘
│ │
└──> (ACTIVE) ──> CANCELLED ──> UNDETERMINED
Lifecycle callbacks map to states:
| Callback | When it fires |
|---|---|
onBegin | UNDETERMINED -> BEGAN (touch detected, not yet recognized) |
onStart | BEGAN -> ACTIVE (gesture recognized, meets criteria) |
onUpdate | While ACTIVE (cumulative: translationX, scale) |
onChange | While ACTIVE (incremental: changeX, changeY) |
onEnd | ACTIVE -> END (finger lifted normally) |
onFinalize | Fires for ANY terminal state (END, FAILED, CANCELLED) |
onEnd vs onFinalize: Use onEnd for success-only cleanup (save position). Use onFinalize for guaranteed cleanup (reset state regardless of outcome). onFinalize receives a second success boolean parameter.
See reference.md for the complete state transition diagram.
Pattern 5: Cross-Component Gesture Relations
When gestures live on different components (parent scroll + child drag), use relation methods:
const childPan = Gesture.Pan();
const parentScroll = Gesture.Native(); // Wraps native ScrollView gesture
// Child drag must fail before parent scroll activates
parentScroll.requireExternalGestureToFail(childPan);
// Or: both active simultaneously
childPan.simultaneousWith(parentScroll);
// Or: child blocks parent while active
childPan.blocksExternalGesture(parentScroll);
Key difference from composition: Gesture.Simultaneous() composes gestures on the same component. .simultaneousWith() relates gestures across different components in the view hierarchy.
See examples/composition.md for cross-component patterns.
Pattern 6: ReanimatedSwipeable for List Rows
Use ReanimatedSwipeable (not the legacy Swipeable) for swipeable list items with smooth 60fps animations.
import ReanimatedSwipeable from "react-native-gesture-handler/ReanimatedSwipeable";
const SWIPE_THRESHOLD = 40;
const OVERSHOOT_FRICTION = 8;
<ReanimatedSwipeable
friction={2}
rightThreshold={SWIPE_THRESHOLD}
overshootFriction={OVERSHOOT_FRICTION}
renderRightActions={renderRightActions}
onSwipeableOpen={handleDelete}
>
<ListItemContent />
</ReanimatedSwipeable>
Why ReanimatedSwipeable over Swipeable: Rewritten with worklets for native-thread animations. The legacy Swipeable animates on JS thread and drops frames during heavy renders.
See examples/swipeable.md for full implementation with action panels and FlatList integration.
Pattern 7: Hover Gesture (Pointer Devices)
Hover gesture detects mouse/stylus/trackpad hovering -- useful for iPad with trackpad, macOS, and web targets. Does not fire on phone touch.
const hover = Gesture.Hover()
.onBegin(() => {
isHovered.value = true;
})
.onEnd(() => {
isHovered.value = false;
});
Platform support: iOS (iPad with trackpad/mouse), macOS, web. Does NOT work on Android touch or iPhone touch. On iOS, the optional hoverEffect prop on GestureDetector provides system-level visual effects (highlight, lift, automatic).
See examples/advanced.md for hover with visual effects.
</patterns><decision_framework>
Decision Framework
Which Gesture Type?
What user interaction are you handling?
├─ Drag/move element → Gesture.Pan()
├─ Single tap → Gesture.Tap()
├─ Double tap → Gesture.Tap().numberOfTaps(2)
├─ Long press → Gesture.LongPress()
├─ Pinch to zoom → Gesture.Pinch()
├─ Two-finger rotate → Gesture.Rotation()
├─ Quick directional swipe → Gesture.Fling()
├─ Mouse/trackpad hover → Gesture.Hover()
└─ Wrap a native gesture → Gesture.Native() (for ScrollView, etc.)
Which Composition?
How should multiple gestures interact?
├─ All active at once (pan + pinch + rotate in photo viewer)
│ └─ Gesture.Simultaneous(gesture1, gesture2, ...)
├─ First to activate wins (swipe vs long-press on same view)
│ └─ Gesture.Race(gesture1, gesture2, ...)
├─ Priority order (double-tap must beat single-tap)
│ └─ Gesture.Exclusive(highPriority, lowPriority)
└─ Gestures on DIFFERENT components (child drag vs parent scroll)
├─ Parent waits for child to fail → parent.requireExternalGestureToFail(child)
├─ Both active simultaneously → child.simultaneousWith(parent)
└─ Child blocks parent → child.blocksExternalGesture(parent)
onChange vs onUpdate?
How are you using the gesture position data?
├─ Adding to an offset (shared value += delta)
│ └─ Use onChange (provides changeX, changeY)
├─ Setting absolute position from gesture start
│ └─ Use onUpdate (provides translationX, translationY)
└─ Need both (rare)
└─ Use onChange for offsets + onUpdate for display values
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Missing
GestureHandlerRootViewat app root -- gestures silently fail with no error message - Using legacy
PanGestureHandler/TapGestureHandlercomponents -- deprecated, useGesture.Pan()+GestureDetector - Using
onUpdateto add deltas to shared values --onUpdateprovides cumulative values, not deltas; useonChangefor incremental updates - Nesting multiple
GestureDetectorcomponents for gestures that should compose -- useGesture.Simultaneous()/Race()/Exclusive()instead - Using
React.PanResponder-- replaced entirely by Gesture Handler; PanResponder runs on JS thread and blocks the UI
Medium Priority Issues:
- Not specifying
minDistanceonGesture.Pan()when coexisting with tap gestures -- pan activates immediately at 0px, stealing taps - Missing
onFinalizecleanup --onEndonly fires on success; cancelled/failed gestures skip it - Using
Swipeableinstead ofReanimatedSwipeable-- legacy component runs animations on JS thread - Creating new gesture instances inside render (not in useMemo or outside component) -- causes gesture to reset every render
Gotchas & Edge Cases:
Gesture.Exclusive(doubleTap, singleTap)-- double-tap MUST be the first argument (higher priority) or it will never fire because single-tap activates first- Android modals need their own
GestureHandlerRootView-- gestures in modals fail silently without it onChangecallback is NOT available in the v3 hooks API -- it was removed; useonUpdatewith thechange*properties on the event object instead- Gesture callbacks are automatically workletized when your animation library is installed -- don't manually add
"worklet"directives unless you need them without the animation library Gesture.Native()wraps platform-native gestures (ScrollView, FlatList) -- use it when you need to create relations between custom gestures and native scrollingonFinalizereceives(event, success)-- thesuccessboolean tells you whether the gesture ended normally (END) or was cancelled/failed- Reusing the same gesture instance across multiple
GestureDetectorcomponents causes undefined behavior -- create separate instances - iPad trackpad two-finger gestures require
enableTrackpadTwoFingerGestureon both Pan andReanimatedSwipeable hoverEffectprop on GestureDetector only works on iOS 17.0+ and provides system-level visual effectsGesture.Fling()only fires at the end of the fling, not continuously -- useGesture.Pan()if you need continuous position tracking during a swipe
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST wrap the app root in GestureHandlerRootView -- gestures will silently fail without it)
(You MUST use GestureDetector with the builder API (Gesture.Pan(), etc.) -- NOT the legacy PanGestureHandler components)
(You MUST use onChange for incremental shared value updates and onUpdate for cumulative values -- mixing them causes drift)
(You MUST use Gesture.Simultaneous() for multi-touch interactions (pinch + pan) -- without it, only one gesture activates)
(You MUST NOT nest GestureDetector components using different API styles (hooks vs builder) under the same root -- this causes undefined behavior)
Failure to follow these rules will cause silent gesture failures, animation drift, and broken multi-touch interactions.
</critical_reminders>
What ships with it: 5 files
37.5 KB alongside SKILL.md
examples/
- advanced.md6.7 KB
- composition.md7.9 KB
- core.md8.2 KB
- swipeable.md6.3 KB
- reference.md8.4 KB