Encodes motion design and animation engineering philosophy for React Native apps using Reanimated, Gesture Handler, and Skia. Use when building or reviewing mobile animations, gestures, transitions, or interactive UI polish. Triggers on animation tasks, micro-interactions, gesture handling, or when the user mentions Reanimated, worklets, shared values, Skia, or mobile motion.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Encodes motion design and animation engineering philosophy for React Native apps using Reanimated, Gesture Handler, and Skia. Use when building or reviewing mobile animations, gestures, transitions, or interactive UI polish. Triggers on animation tasks, micro-interactions, gesture handling, or when the user mentions Reanimated, worklets, shared values, Skia, or mobile motion.
license
MIT
metadata
{"author":"madebyecho","version":"1.0.0"}
React Native Animations
A philosophy and decision framework for building great motion on React Native. Covers Reanimated (values, worklets, layout animations), Gesture Handler (pan, pinch, composition), and Skia (canvas-driven animation) โ the three libraries that make mobile motion feel native in 2026.
This skill is opinionated. It does not cover the legacy Animated API, Moti, or Lottie โ when those are the right tool, this skill does not apply.
When to Apply
Reference these guidelines when:
Building any animation or transition in a React Native or Expo app
Reviewing animation code for performance or polish
Choosing between Reanimated, Skia, or native views for a motion task
Deciding easing curves, durations, or spring parameters
Handling Reduce Motion accessibility settings
Foundational Philosophy
Three principles underlie every decision in this skill. When in doubt, return to these.
1. Motion serves the spatial model. On mobile, animation is not decoration โ it is how users understand where content comes from and where it goes. Modals rise from the button that opened them. Drawers slide from the edge they're anchored to. A swipe that dismisses a card moves with the finger until the gesture ends. If motion does not teach the user something about the interface's structure, it should not exist.
2. The UI thread is sacred. Every animation that touches the JS thread is a visible stutter waiting to happen. Reanimated worklets, shared values, and native gesture handlers exist for exactly one reason: to run motion at 60โ120fps on a thread that never blocks. Any code pattern that forces an animation back to JS is a bug.
3. Motion should match device physics. iOS users expect spring-based, slightly overshooting motion with deceleration-dominant curves. Android/Material users expect the "standard curve" โ quick acceleration, slow deceleration. Don't ship the same linear tween on both platforms and call it cross-platform.
Animation Decision Framework
Four questions, in order. Skip any of them and motion becomes noise.
1. Should this animate?
Frequency of interaction
Recommendation
100+ times/day
No animation. Users want instant.
Dozens/day
Reduce or remove. Consider scale-only feedback.
Occasional
Standard animation.
Rare (onboarding, celebration)
Add delight.
Mobile-specific addition: Does it serve orientation in the navigation stack? A push transition teaches "this came from there." A fade teaches nothing spatial. If the answer is "it serves orientation," animate; otherwise default to instant.
2. What's the purpose?
Valid reasons to animate on mobile:
Spatial continuity between screens or states
Gesture feedback โ the finger is dragging, something should move with it
State indication โ loading, success, error, selection
Preventing jarring layout shifts โ items appearing in a list, keyboard opening
If you cannot name one of these reasons, cut the animation.
3. Which easing?
Defaults that work everywhere:
Use case
Easing
Rationale
Entering (mount, appear)
Easing.out(Easing.cubic)
Decelerates into place โ iOS-feeling
Exiting (unmount, dismiss)
Easing.in(Easing.cubic)
Accelerates away
Moving / morphing
Easing.inOut(Easing.cubic)
Material "standard curve"
Gesture follow
withSpring({ damping: 15, stiffness: 150 })
Physics, not time
Infinite loader
Easing.linear
The only time linear is acceptable
Never use Easing.in or Easing.inOut for mount animations โ delayed initial movement feels sluggish.
For custom curves, prefer cubicBezier(0.23, 1, 0.32, 1) as an iOS-style entering curve. Material's standard curve is cubicBezier(0.4, 0, 0.2, 1).
See references/easing-and-timing.md for the full reference table.
4. Which duration?
Interaction
Duration
Button press / scale feedback
100โ160ms
Toast, icon toggle, small UI
150โ200ms
Navigation push/pop, modal
250โ400ms
Drawer, bottom sheet
300โ500ms
Onboarding illustration
500โ1000ms
Never exceed 500ms for standard navigation. Beyond that, the user perceives lag, not motion. Spring animations don't have a duration โ they have physics, and physics should feel like the device.
The Reanimated Mental Model
Before writing any animation code, internalize these concepts.
useSharedValue โ a value that lives on the UI thread. Mutations don't trigger React re-renders. It is the atom of Reanimated.
const offset = useSharedValue(0);
offset.value = 100; // No re-render. The UI thread sees the new value immediately.
useAnimatedStyle โ a worklet that reads shared values and returns a style object. Runs on the UI thread whenever its shared values change.
Worklets โ functions marked with 'worklet' that run on the UI thread. Gesture callbacks are automatically workletized. To call a JS function from a worklet, wrap it in runOnJS.
const tap = Gesture.Tap().onEnd(() => {
'worklet';
runOnJS(logAnalytics)('tap');
});
Layout animations โ declarative mount/unmount and reorder animations. The easiest path to polish.
Animated.View is not View. Regular View doesn't participate in the worklet render pipeline. Every component that consumes an animated style must be an Animated.* component.
Critical bug to avoid: mutating a shared value inside useAnimatedStyle causes undefined behavior โ potentially an infinite loop. Shared values are written from event handlers, effects, or gesture callbacks โ never from inside the style worklet.
See references/reanimated-patterns.md for API recipes.
Gesture-Driven Animation
Gesture Handler v2 is a declarative API composed with Reanimated. This is where mobile motion becomes mobile.
Velocity matters. A fast flick should dismiss even before the threshold distance. Always read event.velocityX / velocityY in onEnd.
Springs for snap-back. Rubber-band-feeling returns use withSpring, not withTiming.
runOnJS for commit. Navigation, haptics, and analytics all cross the thread boundary via runOnJS.
Haptic coordination: fire Haptics.impactAsync via runOnJS at gesture commit points (activation, snap, dismiss). Never inside onUpdate โ you'll haptic every frame.
Interruption: spring animations must be interruptible. If a new pan starts mid-snap-back, the shared value is simply reassigned and withSpring takes over from the current velocity. Never queue animations with callbacks when you could use withSequence.
See references/gesture-handler-patterns.md for composition recipes and the full pinch+pan+rotate photo viewer example.
Layout Animations & List Transitions
The declarative path โ use these first, before reaching for custom worklets.
Shared tags on a source and destination screen automatically animate geometry between them. Requires the navigator to use the native stack.
Skia for Advanced Motion
Reach for @shopify/react-native-skia when RN views can't do the job:
Charts, graphs, paths, Bรฉzier morphing
Shaders, blur, color filters, blend modes
Particle systems, game loops
Custom gradients and masks
Animated GIFs with frame control (useAnimatedImageValue)
Interop is seamless in modern Skia + Reanimated. Skia components accept shared values and derived values directly โ no separate useValue hook needed:
Time-based animation:useClock() returns a shared value of elapsed milliseconds. Pipe it through useDerivedValue for trigonometric or procedural motion.
Path interpolation:usePathInterpolation(progress, [0, 0.5, 1], [pathA, pathB, pathC]) morphs SVG paths as a shared-value progress moves from 0 to 1.
When not to use Skia: box-model UI (cards, lists, buttons). Skia runs in its own renderer โ you lose accessibility tree integration, native text rendering, and standard touch handling. Use it for the 5% of your app where Reanimated-on-views isn't enough.
See references/skia-animation-patterns.md for canvas recipes.
Performance Rules
Non-negotiables. Violating any of these means a dropped frame somewhere.
Drive animations through shared values, not state. Never setState in an animation loop.
Prefer transform and opacity. Reanimated can animate width, height, and top off-thread (unlike the legacy Animated API), but transforms are still cheaper and never trigger layout reflow of siblings.
Never mutate a shared value inside useAnimatedStyle. Infinite loop / undefined behavior.
Don't setState in onScroll or onGestureEvent. Use useAnimatedScrollHandler and gesture worklets. When you need JS state, use useAnimatedReaction with runOnJS.
Gate hover/press state by platform.hover doesn't exist on touch. Use Pressable states, gesture handlers, or scale feedback.
Fire haptics at commit points, not per frame. Wrap Haptics.impactAsync in runOnJS and call it in onEnd or threshold-crossing branches.
Profile on mid-tier Android, not the iOS simulator. The simulator lies. A real Pixel 6a or Samsung A-series tells the truth.
Watch the UI thread frame rate, not just JS. Perf Monitor shows both. If the UI thread is dropping frames, Reanimated isn't saving you โ there's layout or paint work hiding somewhere.
See assets/performance-checklist.md for the pre-ship verification list.
Accessibility: Reduce Motion
This is not a footnote. Users with vestibular disorders enable Reduce Motion to avoid nausea โ animations that ignore this setting are a physical accessibility failure.
useReducedMotion() (from react-native-reanimated) โ synchronous boolean. Preferred over AccessibilityInfo.isReduceMotionEnabled() because it works in worklets and doesn't require async.
ReducedMotionConfig โ app-wide component. Place it at the root of the app to globally disable animations when the OS setting is on.
Keep opacity, color, and fade animations โ they convey state without vestibular impact.
Remove translate, scale, rotate, and parallax โ collapse to instant or fade.
Example: a slide-up modal becomes a fade-in when Reduce Motion is on. The user still sees the modal appear; they just don't see it travel.
See references/accessibility.md for the full matrix. For audit-mode detection of missing reduce-motion checks, cross-reference the react-native-accessibility skill's p2-reduce-motion-ignored rule.
Component Patterns
The mobile equivalent of Emil Kowalski's "button responsiveness" section. Lift these directly.
Pressable scale feedback (the 160ms "alive" feel):
Toast / snackbar:entering={SlideInDown.springify().damping(18)} + exiting={SlideOutDown.duration(200)}. Never exceed 300ms on either side.
Bottom sheet snap: pan gesture + withSpring with velocity: translateY.value = withSpring(snapPoint, { velocity: event.velocityY, damping: 20, stiffness: 150 }). Snap points at 0, 0.5, 1 of screen height.
Pull-to-refresh:useAnimatedScrollHandler reads event.contentOffset.y, drives a useSharedValue for rubber-band overscroll, fires a refresh via runOnJS at threshold.
Skeleton shimmer:withRepeat(withTiming(1, { duration: 1200 }), -1) drives a translateX on a gradient overlay. Skia gives the nicest gradient; Reanimated-on-view works for simpler skeletons.
Hero / shared element:sharedTransitionTag="id" on source and destination screens. Requires a native stack navigator.
Anti-Patterns
Short, punchy, often-seen, always wrong.
Using the legacy Animated API for interactive elements. It's JS-thread-driven. Use Reanimated.
setState inside onScroll or onGestureEvent. Use worklets + useAnimatedReaction.
Animating marginTop or top instead of translateY. Triggers layout reflow.
Forgetting the 'worklet' directive on callbacks passed to gestures. They run on JS and stutter.
Chaining animations with withTiming(...).then(...)-style callbacks when withSequence exists.
Using LayoutAnimation from core React Native. It's JS-thread-driven and uneven across platforms. Use Reanimated layout animations instead.
Reaching for Skia for a button press. Skia is for canvas work, not UI kit polish.
Driving a Reanimated animation from a React useEffect that depends on a changing prop without reading from a shared value. You're re-creating the animation every render.
Review Format
When reviewing existing animation code, output a markdown table with Before | After | Why columns. Never use separate "Before:" and "After:" lines โ the table makes the tradeoff visible at a glance.
Example:
Before
After
Why
top: withTiming(100)
transform: [{ translateY: withTiming(100) }]
top triggers layout; translateY is compositor-only