src/skills/mobile-animation-gesture-handler/SKILL.md
React Native Gesture Handler - gesture types, GestureDetector, gesture composition, state machine, platform-specific gestures, swipeable rows, hover gestures
npx skillsauth add agents-inc/skills mobile-animation-gesture-handlerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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>
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:
Key patterns covered:
When NOT to use:
Pressable or TouchableOpacity from React Native core)ScrollView or FlatList directly)Detailed Resources:
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:
Simultaneous, Race, ExclusiveGesture.Pan().minDistance(10).onUpdate(handler)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.
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.
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.
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.
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.
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.
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.
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>
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.)
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)
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>
High Priority Issues:
GestureHandlerRootView at app root -- gestures silently fail with no error messagePanGestureHandler/TapGestureHandler components -- deprecated, use Gesture.Pan() + GestureDetectoronUpdate to add deltas to shared values -- onUpdate provides cumulative values, not deltas; use onChange for incremental updatesGestureDetector components for gestures that should compose -- use Gesture.Simultaneous()/Race()/Exclusive() insteadReact.PanResponder -- replaced entirely by Gesture Handler; PanResponder runs on JS thread and blocks the UIMedium Priority Issues:
minDistance on Gesture.Pan() when coexisting with tap gestures -- pan activates immediately at 0px, stealing tapsonFinalize cleanup -- onEnd only fires on success; cancelled/failed gestures skip itSwipeable instead of ReanimatedSwipeable -- legacy component runs animations on JS threadGotchas & Edge Cases:
Gesture.Exclusive(doubleTap, singleTap) -- double-tap MUST be the first argument (higher priority) or it will never fire because single-tap activates firstGestureHandlerRootView -- gestures in modals fail silently without itonChange callback is NOT available in the v3 hooks API -- it was removed; use onUpdate with the change* properties on the event object instead"worklet" directives unless you need them without the animation libraryGesture.Native() wraps platform-native gestures (ScrollView, FlatList) -- use it when you need to create relations between custom gestures and native scrollingonFinalize receives (event, success) -- the success boolean tells you whether the gesture ended normally (END) or was cancelled/failedGestureDetector components causes undefined behavior -- create separate instancesenableTrackpadTwoFingerGesture on both Pan and ReanimatedSwipeablehoverEffect prop 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 -- use Gesture.Pan() if you need continuous position tracking during a swipe</red_flags>
<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>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events