mobile-animation-reanimated
React Native Reanimated 4 - shared values, animated styles, spring/timing/decay, layout animations, gesture integration, scroll-driven animations, interpolation, worklets, CSS animations
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-animation-reanimated/skills/mobile-animation-reanimated
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
React Native Reanimated Patterns
Quick Guide: Reanimated 4 is New Architecture only (requires
react-native-workletsas a separate dependency). UseuseSharedValue+useAnimatedStylefor all animations. Animations run on the UI thread via worklets -- never block the JS thread. UsewithSpring(physics-based) orwithTiming(duration-based) for transitions, layout animations (entering/exiting) for mount/unmount, anduseScrollOffset(renamed fromuseScrollViewOffset) for scroll-driven animations. Reanimated 4 also introduces CSS animations/transitions as a declarative alternative to the worklet API.
<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 use Animated components (Animated.View, Animated.Text, etc.) for any animated styles -- passing animated styles to regular components causes errors)
(You MUST keep static styles in StyleSheet.create and only animate dynamic properties in useAnimatedStyle -- animating static values wastes UI thread resources)
(You MUST NOT mutate shared values inside useAnimatedStyle callbacks -- read only, or you cause infinite loops)
(You MUST use react-native-worklets as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from react-native-reanimated/plugin to react-native-worklets/plugin)
</critical_requirements>
Auto-detection: Reanimated, react-native-reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, Animated.View, Animated.Text, Animated.ScrollView, entering, exiting, FadeIn, FadeOut, SlideIn, interpolate, interpolateColor, useScrollOffset, GestureDetector, Gesture.Pan, worklet, layout animation, shared value, energyThreshold, CSS animation, react-native-worklets
When to use:
- Animating view properties (opacity, transforms, colors) on the UI thread
- Adding entering/exiting animations when components mount/unmount
- Building gesture-driven animations (drag, swipe, pinch)
- Creating scroll-driven header collapse, parallax, or sticky effects
- Interpolating values across ranges (position to opacity, scroll to scale)
- Implementing spring physics or timing-based transitions
When NOT to use:
- Simple boolean show/hide without animation (conditional rendering suffices)
- Static layouts that never change at runtime
- Animated.Value from React Native core (use Reanimated's shared values instead)
Key patterns covered:
- Shared values (
useSharedValue) + animated styles (useAnimatedStyle) - Animation functions:
withTiming,withSpring,withDecay - Layout animations:
entering/exitingwith predefined builders - Gesture integration:
Gesture.Pan+ shared values +withDecay - Scroll-driven animations with
useScrollOffset - Interpolation:
interpolateandinterpolateColor - Worklet functions and the
'worklet'directive - CSS animations and transitions (Reanimated 4 declarative API)
Detailed Resources:
- examples/core.md - Shared values, animated styles, withTiming, withSpring, interpolation
- examples/layout-animations.md - Entering/exiting animations, predefined builders, custom layout animations
- examples/gestures.md - Pan gesture with shared values, swipe-to-dismiss, withDecay
- examples/scroll-animations.md - Collapsing header, parallax, scroll-driven opacity
- reference.md - Decision frameworks, migration from 3.x, API quick reference
<red_flags>
RED FLAGS
High Priority Issues:
- Passing animated styles to regular
View/Textinstead ofAnimated.View/Animated.Text-- causes silent failure or crash - Mutating shared values inside
useAnimatedStyle-- causes infinite re-evaluation loops - Using
react-native-reanimated/pluginin Babel config with Reanimated 4 -- must usereact-native-worklets/plugin(and it must be last in the plugins array) - Using Reanimated 4.x with Legacy Architecture (old bridge) -- Reanimated 4 is New Architecture only
- Animating static properties in
useAnimatedStyleinstead of keeping them inStyleSheet-- wastes UI thread resources - Using
restDisplacementThreshold/restSpeedThresholdinwithSpring-- removed in v4, replaced byenergyThreshold
Medium Priority Issues:
- Creating layout animation builders inline in render -- allocates objects every render; define outside component or in
useMemo - Using
runOnJS/runOnUIinstead ofscheduleOnRN/scheduleOnUI-- old API moved toreact-native-worklets - Missing
GestureHandlerRootViewat app root -- gestures silently fail without it - Using
useAnimatedGestureHandler-- removed in v4, migrate to Gesture Handler 2'sGestureAPI
Gotchas and Edge Cases:
withSpringduration: actual completion time = perceptualdurationx 1.5 -- divide v3 duration values by 1.5 when migratinguseScrollOffsetrenamed fromuseScrollViewOffset-- deprecated alias still works temporarily- Shared value
.valueaccess is synchronous on UI thread but asynchronous on JS thread -- don't rely on immediate reads after writes on JS thread useWorkletCallbackremoved -- replace withuseCallback+'worklet'directive- React Compiler compatibility: use
sv.get()andsv.set()instead of direct.valueaccess when using React Compiler combineTransitionremoved -- useEntryExitTransition.entering(entering).exiting(exiting)- Object shared values: reassign the entire object, never mutate individual properties -- mutations break reactivity tracking
- Removing an animated style from a view does not unset the animated values -- explicitly set properties to
undefinedto reset - On New Architecture, layout animations use
nativeIDinternally -- don't overwrite it on animated components
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST use Animated components (Animated.View, Animated.Text, etc.) for any animated styles -- passing animated styles to regular components causes errors)
(You MUST keep static styles in StyleSheet.create and only animate dynamic properties in useAnimatedStyle -- animating static values wastes UI thread resources)
(You MUST NOT mutate shared values inside useAnimatedStyle callbacks -- read only, or you cause infinite loops)
(You MUST use react-native-worklets as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from react-native-reanimated/plugin to react-native-worklets/plugin)
Failure to follow these rules will cause animation failures, infinite loops, and crashes on the UI thread.
</critical_reminders>
Files (skills)
-
examples
-
core.md 10.3 KB
# Reanimated - Core Patterns > Shared values, animated styles, animation functions, and interpolation. See [SKILL.md](../SKILL.md) for decision guidance and red flags. **Prerequisites:** Reanimated 4 installed with `react-native-worklets`. New Architecture enabled (React Native 0.76+). --- ## Pattern 1: Shared Value + Animated Style Basics ```typescript import { useState } from "react"; import { Pressable, Text, StyleSheet } from "react-native"; import Animated, { useSharedValue, useAnimatedStyle, withTiming, } from "react-native-reanimated"; const BOX_SIZE = 100; const SCALE_ACTIVE = 1.2; const SCALE_INACTIVE = 1; const OPACITY_ACTIVE = 1; const OPACITY_INACTIVE = 0.5; const ANIMATION_DURATION = 300; export function AnimatedBox() { const [active, setActive] = useState(false); const scale = useSharedValue(SCALE_INACTIVE); const opacity = useSharedValue(OPACITY_INACTIVE); // Only dynamic properties in useAnimatedStyle const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], opacity: opacity.value, })); const handlePress = () => { const nextActive = !active; setActive(nextActive); scale.value = withTiming( nextActive ? SCALE_ACTIVE : SCALE_INACTIVE, { duration: ANIMATION_DURATION } ); opacity.value = withTiming( nextActive ? OPACITY_ACTIVE : OPACITY_INACTIVE, { duration: ANIMATION_DURATION } ); }; return ( <Pressable onPress={handlePress}> {/* Static styles in StyleSheet, dynamic in animatedStyle */} <Animated.View style={[styles.box, animatedStyle]}> <Text>{active ? "Active" : "Inactive"}</Text> </Animated.View> </Pressable> ); } const styles = StyleSheet.create({ box: { width: BOX_SIZE, height: BOX_SIZE, backgroundColor: "#3498db", borderRadius: 8, alignItems: "center", justifyContent: "center", }, }); ``` **Why good:** static styles (width, height, backgroundColor, borderRadius) stay in StyleSheet, only dynamic properties (scale, opacity) in useAnimatedStyle, named constants for all values --- ## Pattern 2: React Compiler Compatibility When using React Compiler, use `get()` and `set()` instead of direct `.value` access to avoid the compiler misinterpreting shared value reactivity. ```typescript import Animated, { useSharedValue, useAnimatedStyle, withSpring, } from "react-native-reanimated"; const TARGET_POSITION = 200; const INITIAL_POSITION = 0; export function CompilerSafeAnimation() { const translateX = useSharedValue(INITIAL_POSITION); // get() instead of .value in animated style const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.get() }], })); const handlePress = () => { // set() with updater function instead of .value assignment translateX.set((current) => current === INITIAL_POSITION ? TARGET_POSITION : INITIAL_POSITION ); }; return ( <Pressable onPress={handlePress}> <Animated.View style={[styles.box, animatedStyle]} /> </Pressable> ); } ``` **Why good:** `get()`/`set()` are React Compiler safe -- direct `.value` access may be misinterpreted by the compiler's memoization analysis **When to use:** only when your project uses React Compiler. Direct `.value` access remains valid without the compiler. --- ## Pattern 3: withSpring Configuration Two modes: physics-based (damping/stiffness) and duration-based (duration/dampingRatio). Don't mix parameters from both modes. ```typescript import { useSharedValue, withSpring } from "react-native-reanimated"; const TARGET = 100; // Physics-based: natural feel, bouncy // Lower damping = more bouncy, higher stiffness = snappier function physicsSpring(sv: ReturnType<typeof useSharedValue<number>>) { sv.value = withSpring(TARGET, { damping: 80, // how quickly it settles (default: 120) stiffness: 600, // how bouncy (default: 900) mass: 1, // weight (default: 4) }); } // Duration-based: predictable timing with spring feel // dampingRatio < 1 = bouncy, 1 = critically damped (no overshoot), > 1 = overdamped function durationSpring(sv: ReturnType<typeof useSharedValue<number>>) { sv.value = withSpring(TARGET, { duration: 400, // perceptual duration in ms (default: 550) dampingRatio: 0.7, // < 1 bouncy, 1 no bounce, > 1 overdamped }); } // Clamped spring: prevents overshoot past boundaries function clampedSpring(sv: ReturnType<typeof useSharedValue<number>>) { sv.value = withSpring(TARGET, { duration: 400, dampingRatio: 0.7, clamp: { min: 0, max: 120 }, // limits movement range }); } // With completion callback function springWithCallback(sv: ReturnType<typeof useSharedValue<number>>) { sv.value = withSpring(TARGET, { damping: 100 }, (finished) => { "worklet"; if (finished) { // Animation completed -- safe to trigger next animation } }); } ``` **Why good:** clear separation of physics-based vs duration-based configs, clamp prevents visual overshoot, callback for chaining **Migration note:** `restDisplacementThreshold` and `restSpeedThreshold` are removed in v4. The new `energyThreshold` (default `6e-9`) is relative to the animation. In most cases, just delete the old threshold parameters. --- ## Pattern 4: withTiming and Easing ```typescript import { withTiming, Easing, withSequence, withRepeat, withDelay, } from "react-native-reanimated"; const ANIMATION_DURATION = 300; const FADE_DELAY = 200; const SHAKE_OFFSET = 10; const SHAKE_DURATION = 80; const SHAKE_REPETITIONS = 3; // Basic timing with easing function fadeIn(sv: SharedValue<number>) { sv.value = withTiming(1, { duration: ANIMATION_DURATION, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1), }); } // Delayed animation function delayedFade(sv: SharedValue<number>) { sv.value = withDelay( FADE_DELAY, withTiming(1, { duration: ANIMATION_DURATION }), ); } // Sequence: shake animation function shake(sv: SharedValue<number>) { sv.value = withSequence( withTiming(-SHAKE_OFFSET, { duration: SHAKE_DURATION }), withRepeat( withTiming(SHAKE_OFFSET, { duration: SHAKE_DURATION }), SHAKE_REPETITIONS, true, // reverse each iteration ), withTiming(0, { duration: SHAKE_DURATION }), ); } ``` **Easing presets:** `Easing.linear`, `Easing.ease`, `Easing.quad`, `Easing.cubic`, `Easing.bezierFn(x1, y1, x2, y2)`, `Easing.inOut(Easing.quad)`, `Easing.in(Easing.elastic(1))`, `Easing.out(Easing.bounce)`. --- ## Pattern 5: withDecay (Momentum) Simulates friction-based deceleration. Commonly used after gesture fling to maintain momentum. ```typescript import { withDecay, withClamp } from "react-native-reanimated"; const MIN_POSITION = 0; const MAX_POSITION = 300; // Basic decay with velocity from gesture function applyDecay(sv: SharedValue<number>, velocity: number) { sv.value = withDecay({ velocity, rubberBandEffect: true, // bounces at edges instead of hard stop clamp: [MIN_POSITION, MAX_POSITION], // boundaries }); } // Alternative: withClamp modifier wrapping other animations function clampedDecay(sv: SharedValue<number>, velocity: number) { sv.value = withClamp( { min: MIN_POSITION, max: MAX_POSITION }, withDecay({ velocity }), ); } ``` **Why good:** `rubberBandEffect` gives iOS-like elastic boundary behavior, `clamp` prevents values escaping range --- ## Pattern 6: Interpolation Map value ranges. Use in `useAnimatedStyle` for scroll-driven or gesture-driven transformations. ```typescript import { interpolate, interpolateColor, Extrapolation, useAnimatedStyle, } from "react-native-reanimated"; const SCROLL_THRESHOLD = 100; const HEADER_MAX = 200; const HEADER_MIN = 60; // Number interpolation with clamping function useHeaderStyle(scrollY: SharedValue<number>) { return useAnimatedStyle(() => { const height = interpolate( scrollY.value, [0, HEADER_MAX - HEADER_MIN], [HEADER_MAX, HEADER_MIN], Extrapolation.CLAMP, ); const opacity = interpolate( scrollY.value, [0, SCROLL_THRESHOLD], [1, 0], Extrapolation.CLAMP, ); return { height, opacity }; }); } // Color interpolation: progress bar changes color function useProgressColor(progress: SharedValue<number>) { return useAnimatedStyle(() => { const backgroundColor = interpolateColor( progress.value, [0, 0.5, 1], ["#FF3B30", "#FFCC00", "#34C759"], // red -> yellow -> green ); return { backgroundColor }; }); } // Mixed left/right extrapolation function useParallax(scrollY: SharedValue<number>) { return useAnimatedStyle(() => { const translateY = interpolate( scrollY.value, [0, SCROLL_THRESHOLD], [0, -50], { extrapolateLeft: Extrapolation.CLAMP, // don't go past 0 extrapolateRight: Extrapolation.EXTEND, // continue beyond range }, ); return { transform: [{ translateY }] }; }); } ``` **Extrapolation types:** | Type | Behavior | | ---------- | ----------------------------- | | `CLAMP` | Caps output at range edges | | `EXTEND` | Continues linearly past range | | `IDENTITY` | Returns the raw input value | **`interpolateColor` modes:** `'RGB'` (default) or `'HSV'`. HSV produces perceptually smoother hue transitions (e.g., red to blue through purple, not through muddy brown). --- ## Pattern 7: Animated Text and Animated.createAnimatedComponent Not all components have `Animated.*` wrappers. Use `Animated.createAnimatedComponent` for custom components. ```typescript import Animated from "react-native-reanimated"; import { TextInput } from "react-native"; // Built-in: Animated.View, Animated.Text, Animated.ScrollView, Animated.Image, Animated.FlatList // Custom: wrap any component that accepts style prop const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); // Usage function AnimatedSearch() { const width = useSharedValue(200); const style = useAnimatedStyle(() => ({ width: width.value, })); return <AnimatedTextInput style={[styles.input, style]} placeholder="Search..." />; } ``` **Why good:** extends Reanimated to any component that accepts a `style` prop, type-safe with the wrapped component's props **Gotcha:** `createAnimatedComponent` should be called at module level, not inside a component -- calling it in render creates a new component type each time, breaking React reconciliation. -
gestures.md 7.4 KB
# Reanimated - Gesture Integration > Gesture-driven animations using react-native-gesture-handler + Reanimated shared values. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for shared values and withDecay. **Note:** This file covers Reanimated's side of gesture-driven animations. For gesture configuration details (tap, pinch, fling, simultaneous/exclusive gestures), see the gesture handler skill. --- ## Pattern 1: Draggable Element with Pan Gesture ```typescript import { StyleSheet } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useSharedValue, useAnimatedStyle, withSpring, } from "react-native-reanimated"; const INITIAL_POSITION = 0; export function DraggableCircle() { const translateX = useSharedValue(INITIAL_POSITION); const translateY = useSharedValue(INITIAL_POSITION); const savedX = useSharedValue(INITIAL_POSITION); const savedY = useSharedValue(INITIAL_POSITION); const pan = Gesture.Pan() .onStart(() => { // Save position at gesture start for relative movement savedX.value = translateX.value; savedY.value = translateY.value; }) .onUpdate((e) => { // Absolute translation from gesture start translateX.value = savedX.value + e.translationX; translateY.value = savedY.value + e.translationY; }) .onEnd(() => { // Spring back to origin translateX.value = withSpring(INITIAL_POSITION); translateY.value = withSpring(INITIAL_POSITION); }); const animatedStyle = useAnimatedStyle(() => ({ transform: [ { translateX: translateX.value }, { translateY: translateY.value }, ], })); return ( <GestureDetector gesture={pan}> <Animated.View style={[styles.circle, animatedStyle]} /> </GestureDetector> ); } const styles = StyleSheet.create({ circle: { width: 80, height: 80, borderRadius: 40, backgroundColor: "#3498db", }, }); ``` **Why good:** `onStart` saves position for relative movement, `onUpdate` uses absolute translationX/Y from gesture start, `onEnd` springs back, gesture callbacks are auto-workletized ### onUpdate vs onChange - **`onUpdate`** -- receives absolute values (`translationX`, `translationY`) relative to gesture start - **`onChange`** -- receives delta values (`changeX`, `changeY`) since last event Use `onChange` when accumulating position incrementally. Use `onUpdate` when setting position from a known start point. --- ## Pattern 2: Swipe-to-Dismiss ```typescript import { Dimensions, StyleSheet, Text } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, Extrapolation, type SharedValue, } from "react-native-reanimated"; import { scheduleOnRN } from "react-native-worklets"; const SCREEN_WIDTH = Dimensions.get("window").width; const DISMISS_THRESHOLD = SCREEN_WIDTH * 0.3; const DISMISS_VELOCITY = 500; const ANIMATION_DURATION = 200; interface SwipeableCardProps { onDismiss: () => void; children: React.ReactNode; } export function SwipeableCard({ onDismiss, children }: SwipeableCardProps) { const translateX = useSharedValue(0); const pan = Gesture.Pan() .activeOffsetX([-10, 10]) // require 10px horizontal movement to activate .onChange((e) => { translateX.value += e.changeX; }) .onFinalize((e) => { const shouldDismiss = Math.abs(translateX.value) > DISMISS_THRESHOLD || Math.abs(e.velocityX) > DISMISS_VELOCITY; if (shouldDismiss) { const direction = translateX.value > 0 ? 1 : -1; translateX.value = withTiming( direction * SCREEN_WIDTH, { duration: ANIMATION_DURATION }, () => { // scheduleOnRN to call JS function from UI thread scheduleOnRN(onDismiss); } ); } else { // Snap back translateX.value = withTiming(0, { duration: ANIMATION_DURATION }); } }); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], opacity: interpolate( Math.abs(translateX.value), [0, SCREEN_WIDTH], [1, 0.5], Extrapolation.CLAMP ), })); return ( <GestureDetector gesture={pan}> <Animated.View style={[styles.card, animatedStyle]}> {children} </Animated.View> </GestureDetector> ); } const styles = StyleSheet.create({ card: { padding: 16, backgroundColor: "#FFFFFF", borderRadius: 12, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, }); ``` **Why good:** `activeOffsetX` prevents accidental activation during vertical scroll, velocity check catches fast flicks, `scheduleOnRN` bridges from UI thread callback to JS, opacity fades with distance **Key points:** - `scheduleOnRN` (Reanimated 4) replaces `runOnJS` for calling JS functions from worklets - `onFinalize` fires when gesture ends regardless of success/failure -- safer than `onEnd` for cleanup - `activeOffsetX` with symmetric values prevents horizontal pan from stealing vertical scroll --- ## Pattern 3: Gesture with Decay (Fling Momentum) ```typescript import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useSharedValue, useAnimatedStyle, withDecay, } from "react-native-reanimated"; const MIN_X = 0; const MAX_X = 300; export function FlickableElement() { const offsetX = useSharedValue(0); const pan = Gesture.Pan() .onChange((e) => { offsetX.value += e.changeX; }) .onFinalize((e) => { // withDecay preserves momentum from the fling offsetX.value = withDecay({ velocity: e.velocityX, rubberBandEffect: true, // elastic bounce at boundaries clamp: [MIN_X, MAX_X], // movement boundaries }); }); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offsetX.value }], })); return ( <GestureDetector gesture={pan}> <Animated.View style={[styles.element, animatedStyle]} /> </GestureDetector> ); } ``` **Why good:** `withDecay` takes velocity directly from gesture event, `rubberBandEffect` gives iOS-like elastic boundary, `clamp` prevents element from escaping bounds --- ## Pattern 4: Scale on Press (Tap Gesture) ```typescript import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useSharedValue, useAnimatedStyle, withSpring, } from "react-native-reanimated"; const SCALE_PRESSED = 0.95; const SCALE_DEFAULT = 1; export function PressableCard({ children }: { children: React.ReactNode }) { const scale = useSharedValue(SCALE_DEFAULT); const tap = Gesture.Tap() .onBegin(() => { scale.value = withSpring(SCALE_PRESSED, { damping: 100, stiffness: 800 }); }) .onFinalize(() => { scale.value = withSpring(SCALE_DEFAULT, { damping: 100, stiffness: 800 }); }); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], })); return ( <GestureDetector gesture={tap}> <Animated.View style={[styles.card, animatedStyle]}> {children} </Animated.View> </GestureDetector> ); } ``` **Why good:** `onBegin` fires immediately on touch (not on release), `onFinalize` restores scale whether tap completes or cancels, spring gives natural bounce feel -
layout-animations.md 6.3 KB
# Reanimated - Layout Animations > Entering/exiting animations, predefined builders, custom animations. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for shared values and animation functions. --- ## Pattern 1: Predefined Entering/Exiting Apply to `Animated.*` components. Component must be conditionally rendered (mount/unmount triggers the animation). ```typescript import { useState } from "react"; import { Pressable, Text, StyleSheet, View } from "react-native"; import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft, ZoomIn, BounceIn, } from "react-native-reanimated"; const ANIMATION_DURATION = 400; const STAGGER_DELAY = 100; export function NotificationList({ items }: { items: Notification[] }) { return ( <View style={styles.list}> {items.map((item, index) => ( <Animated.View key={item.id} entering={SlideInRight.delay(index * STAGGER_DELAY).duration(ANIMATION_DURATION)} exiting={SlideOutLeft.duration(ANIMATION_DURATION)} style={styles.item} > <Text>{item.message}</Text> </Animated.View> ))} </View> ); } const styles = StyleSheet.create({ list: { gap: 8 }, item: { padding: 16, backgroundColor: "#F0F0F0", borderRadius: 8, }, }); ``` **Why good:** staggered delay based on index creates cascade effect, separate entering/exiting animations for different UX feel ### Available Builder Families | Family | Entering | Exiting | Variants | | ---------- | ------------------- | -------------------- | --------------------------------------- | | Fade | `FadeIn` | `FadeOut` | Up, Down, Left, Right | | Slide | `SlideInRight` | `SlideOutLeft` | Up, Down, Left, Right | | Zoom | `ZoomIn` | `ZoomOut` | Up, Down, Left, Right, EasyUp, EasyDown | | Bounce | `BounceIn` | `BounceOut` | Up, Down, Left, Right | | Flip | `FlipInEasyX` | `FlipOutEasyX` | X, Y, XUp, XDown, YLeft, YRight | | LightSpeed | `LightSpeedInRight` | `LightSpeedOutRight` | Left, Right | | Rotate | `RotateInDownLeft` | `RotateOutDownLeft` | Up/Down + Left/Right | | Pinwheel | `PinwheelIn` | `PinwheelOut` | (no variants) | | Roll | `RollInLeft` | `RollOutLeft` | Left, Right | | Stretch | `StretchInX` | `StretchOutX` | X, Y | --- ## Pattern 2: Modifier Chaining Customize predefined animations with modifier methods. ```typescript import Animated, { FadeIn, SlideInUp, BounceIn } from "react-native-reanimated"; const ENTER_DURATION = 500; const EXIT_DURATION = 300; const ENTER_DELAY = 200; // Duration + easing <Animated.View entering={FadeIn.duration(ENTER_DURATION).easing(Easing.bezierFn(0.25, 0.1, 0.25, 1))} /> // Spring-based (replaces default timing with spring physics) <Animated.View entering={BounceIn.springify().damping(12).stiffness(200)} /> // Delay + callback <Animated.View entering={SlideInUp.delay(ENTER_DELAY).withCallback((finished) => { "worklet"; if (finished) { // Animation completed on UI thread } })} /> // Override initial values <Animated.View entering={FadeIn.withInitialValues({ opacity: 0.3, transform: [{ scale: 0.8 }] })} /> // Accessibility: respect reduced motion preference <Animated.View entering={FadeIn.reduceMotion(ReduceMotion.System)} /> ``` **Performance tip:** Define builders outside the component or memoize them -- inline creation in JSX allocates new objects every render. ```typescript // Good: defined once at module level const ENTER_ANIMATION = FadeIn.duration(400).delay(100); const EXIT_ANIMATION = FadeOut.duration(300); function Card() { return <Animated.View entering={ENTER_ANIMATION} exiting={EXIT_ANIMATION} />; } ``` --- ## Pattern 3: EntryExitTransition (Replacing combineTransition) `combineTransition` was removed in v4. Use `EntryExitTransition` to combine different entering and exiting animations into a single layout transition. ```typescript import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft, EntryExitTransition, } from "react-native-reanimated"; // Combine different entering and exiting animations const LAYOUT_TRANSITION = EntryExitTransition .entering(SlideInRight) .exiting(FadeOut); function AnimatedList({ items }: { items: Item[] }) { return ( <View> {items.map((item) => ( <Animated.View key={item.id} layout={LAYOUT_TRANSITION} style={styles.item} > <Text>{item.name}</Text> </Animated.View> ))} </View> ); } ``` --- ## Pattern 4: Custom Entering/Exiting Animation Build fully custom animations when predefined builders don't fit. ```typescript import Animated, { withTiming, withSpring, type EntryAnimationsValues } from "react-native-reanimated"; const CUSTOM_DURATION = 600; const INITIAL_OFFSET = 50; // Custom entering animation function function customEnteringAnimation(values: EntryAnimationsValues) { "worklet"; const animations = { opacity: withTiming(1, { duration: CUSTOM_DURATION }), transform: [ { translateY: withSpring(0, { damping: 100 }) }, { scale: withSpring(1, { damping: 80 }) }, ], }; const initialValues = { opacity: 0, transform: [ { translateY: INITIAL_OFFSET }, { scale: 0.9 }, ], }; return { initialValues, animations }; } function CustomAnimatedCard() { return ( <Animated.View entering={customEnteringAnimation} style={styles.card}> <Text>Custom animation</Text> </Animated.View> ); } ``` **Why good:** full control over initial values and animation config per property, can mix spring and timing in one entering animation, `values` parameter provides target layout measurements (width, height, targetOriginX, etc.) **Custom animation callback shape:** - Receives `EntryAnimationsValues` (entering) or `ExitAnimationsValues` (exiting) with target layout info - Must return `{ initialValues, animations }` where each has the same style property keys - Must be marked with `'worklet'` directive -
scroll-animations.md 7 KB
# Reanimated - Scroll-Driven Animations > Scroll-driven animations with useScrollOffset, collapsing headers, parallax. See [SKILL.md](../SKILL.md) for decision guidance. **Related:** [core.md](core.md) for interpolation patterns. --- ## Pattern 1: Collapsing Header Track scroll offset and interpolate header height, title opacity, and background. ```typescript import { StyleSheet, Text, View } from "react-native"; import Animated, { useAnimatedRef, useScrollOffset, useAnimatedStyle, interpolate, interpolateColor, Extrapolation, } from "react-native-reanimated"; const HEADER_MAX_HEIGHT = 200; const HEADER_MIN_HEIGHT = 60; const SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT; const TITLE_FONT_MAX = 28; const TITLE_FONT_MIN = 18; export function CollapsibleHeaderScreen() { const scrollRef = useAnimatedRef<Animated.ScrollView>(); const scrollOffset = useScrollOffset(scrollRef); const headerStyle = useAnimatedStyle(() => ({ height: interpolate( scrollOffset.value, [0, SCROLL_DISTANCE], [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT], Extrapolation.CLAMP ), backgroundColor: interpolateColor( scrollOffset.value, [0, SCROLL_DISTANCE], ["rgba(0,0,0,0)", "rgba(0,0,0,0.9)"] ), })); const titleStyle = useAnimatedStyle(() => ({ fontSize: interpolate( scrollOffset.value, [0, SCROLL_DISTANCE], [TITLE_FONT_MAX, TITLE_FONT_MIN], Extrapolation.CLAMP ), opacity: interpolate( scrollOffset.value, [0, SCROLL_DISTANCE * 0.5, SCROLL_DISTANCE], [1, 0.8, 1], Extrapolation.CLAMP ), })); return ( <View style={styles.container}> <Animated.View style={[styles.header, headerStyle]}> <Animated.Text style={[styles.title, titleStyle]}> Profile </Animated.Text> </Animated.View> <Animated.ScrollView ref={scrollRef} contentContainerStyle={{ paddingTop: HEADER_MAX_HEIGHT }} scrollEventThrottle={16} > {/* Scroll content */} </Animated.ScrollView> </View> ); } const styles = StyleSheet.create({ container: { flex: 1 }, header: { position: "absolute", top: 0, left: 0, right: 0, zIndex: 1, justifyContent: "flex-end", paddingHorizontal: 16, paddingBottom: 12, }, title: { color: "#FFFFFF", fontWeight: "bold", }, }); ``` **Why good:** `useScrollOffset` auto-detects scroll direction, `Extrapolation.CLAMP` prevents over/under-shoot, font size and opacity give depth to the collapse **Key detail:** `scrollEventThrottle={16}` is needed on `ScrollView` for smooth 60fps tracking. Without it, scroll events fire less frequently. --- ## Pattern 2: Parallax Image Image moves at a slower rate than content, creating depth. ```typescript import { StyleSheet, Text, View, Dimensions } from "react-native"; import Animated, { useAnimatedRef, useScrollOffset, useAnimatedStyle, interpolate, Extrapolation, } from "react-native-reanimated"; const IMAGE_HEIGHT = 300; const PARALLAX_FACTOR = 0.5; // image moves at half the scroll speed export function ParallaxScreen() { const scrollRef = useAnimatedRef<Animated.ScrollView>(); const scrollOffset = useScrollOffset(scrollRef); const imageStyle = useAnimatedStyle(() => ({ transform: [ { translateY: interpolate( scrollOffset.value, [0, IMAGE_HEIGHT], [0, IMAGE_HEIGHT * PARALLAX_FACTOR], Extrapolation.CLAMP ), }, ], })); const overlayStyle = useAnimatedStyle(() => ({ opacity: interpolate( scrollOffset.value, [0, IMAGE_HEIGHT * 0.5], [0, 0.7], Extrapolation.CLAMP ), })); return ( <Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}> <View style={styles.imageContainer}> <Animated.Image source={{ uri: "https://example.com/hero.jpg" }} style={[styles.image, imageStyle]} /> <Animated.View style={[styles.overlay, overlayStyle]} /> </View> <View style={styles.content}> <Text>Content below parallax image</Text> </View> </Animated.ScrollView> ); } const styles = StyleSheet.create({ imageContainer: { height: IMAGE_HEIGHT, overflow: "hidden", }, image: { width: "100%", height: IMAGE_HEIGHT * 1.5, // taller than container for parallax room }, overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: "#000000", }, content: { padding: 16, backgroundColor: "#FFFFFF", }, }); ``` **Why good:** `PARALLAX_FACTOR` controls depth effect intensity, image is taller than container for scroll room, overlay fades in as image scrolls away --- ## Pattern 3: Scroll-to-Hide Tab Bar Tab bar slides down and fades out when scrolling down, reappears when scrolling up. ```typescript import { StyleSheet, View, Text } from "react-native"; import Animated, { useAnimatedRef, useScrollOffset, useSharedValue, useAnimatedStyle, withTiming, interpolate, Extrapolation, } from "react-native-reanimated"; const TAB_BAR_HEIGHT = 60; const SCROLL_THRESHOLD = 10; const ANIMATION_DURATION = 200; export function HidingTabBar() { const scrollRef = useAnimatedRef<Animated.ScrollView>(); const scrollOffset = useScrollOffset(scrollRef); const prevOffset = useSharedValue(0); const tabBarTranslateY = useSharedValue(0); // Detect scroll direction and animate tab bar const animatedTabStyle = useAnimatedStyle(() => { const diff = scrollOffset.value - prevOffset.value; prevOffset.value = scrollOffset.value; if (diff > SCROLL_THRESHOLD) { // Scrolling down: hide tabBarTranslateY.value = withTiming(TAB_BAR_HEIGHT, { duration: ANIMATION_DURATION, }); } else if (diff < -SCROLL_THRESHOLD) { // Scrolling up: show tabBarTranslateY.value = withTiming(0, { duration: ANIMATION_DURATION, }); } return { transform: [{ translateY: tabBarTranslateY.value }], opacity: interpolate( tabBarTranslateY.value, [0, TAB_BAR_HEIGHT], [1, 0], Extrapolation.CLAMP ), }; }); return ( <View style={styles.container}> <Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}> {/* content */} </Animated.ScrollView> <Animated.View style={[styles.tabBar, animatedTabStyle]}> <Text>Tab 1</Text> <Text>Tab 2</Text> <Text>Tab 3</Text> </Animated.View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1 }, tabBar: { position: "absolute", bottom: 0, left: 0, right: 0, height: TAB_BAR_HEIGHT, flexDirection: "row", justifyContent: "space-around", alignItems: "center", backgroundColor: "#FFFFFF", borderTopWidth: 1, borderTopColor: "#E0E0E0", }, }); ``` **Why good:** threshold prevents jittery show/hide on small scrolls, opacity interpolated from translateY for coordinated animation, previous offset tracked to detect direction
-
-
reference.md 7.6 KB
# Reanimated Quick Reference > Decision frameworks, migration guide, and API quick reference. See [SKILL.md](SKILL.md) for red flags and patterns. --- ## Decision Framework ### Which Animation Function? ``` What drives the animation? | +-> State change (boolean toggle, prop change)? | +-> Simple transition? -> CSS transitions (Reanimated 4) or withTiming | +-> Need spring feel? -> withSpring | +-> Complex keyframes? -> CSS animations (Reanimated 4) or withSequence | +-> User gesture (drag, swipe, pinch)? | +-> During gesture? -> Direct shared value update in gesture callback | +-> After gesture release? -> withDecay (momentum) or withSpring (snap back) | +-> Scroll position? | +-> useScrollOffset + interpolate in useAnimatedStyle | +-> Component mount/unmount? +-> Predefined? -> entering={FadeIn} exiting={FadeOut} +-> Custom? -> entering={customFn} with 'worklet' directive ``` ### CSS Animations vs Worklets ``` Which API to use? | +-> State-driven (toggle, prop change)? | +-> CSS transitions/animations (less code, better optimizable) | +-> Gesture-driven (drag, swipe)? | +-> Worklet API (shared values + gesture callbacks) | +-> Scroll-driven (parallax, collapse)? | +-> Worklet API (useScrollOffset + interpolate) | +-> Frame-by-frame control needed? | +-> Worklet API (full control) | +-> Mixed? +-> Both work simultaneously -- use CSS for simple parts, worklets for complex ``` ### withSpring Configuration ``` What kind of spring behavior? | +-> Need precise timing control? | +-> Duration-based: { duration: 500, dampingRatio: 0.8 } | +-> dampingRatio < 1 = bouncy, 1 = no overshoot, > 1 = overdamped | +-> Need natural physics feel? | +-> Physics-based: { damping: 100, stiffness: 800 } | +-> Lower damping = more bouncy, higher stiffness = snappier | +-> Need to prevent overshoot? | +-> Add overshootClamping: true | +-> Or use clamp: { min: 0, max: 200 } | +-> Don't mix physics-based and duration-based params ``` --- ## Migration from 3.x to 4.x ### Dependency Changes | Step | 3.x | 4.x | | ------------ | -------------------------------- | --------------------------------------------------- | | Install | `react-native-reanimated` | `react-native-reanimated` + `react-native-worklets` | | Babel plugin | `react-native-reanimated/plugin` | `react-native-worklets/plugin` (must be last) | | Architecture | Both old and new | New Architecture only | ### API Renames | 3.x | 4.x | Package | | ----------------------------------- | ---------------------------------- | ------------------------- | | `runOnJS(fn)("arg")` | `scheduleOnRN(fn, "arg")` | `react-native-worklets` | | `runOnUI(fn)("arg")` | `scheduleOnUI(fn, "arg")` | `react-native-worklets` | | `executeOnUIRuntimeSync(fn)("arg")` | `runOnUISync(fn, "arg")` | `react-native-worklets` | | `runOnRuntime(rt, fn)("arg")` | `scheduleOnRuntime(rt, fn, "arg")` | `react-native-worklets` | | `useScrollViewOffset` | `useScrollOffset` | `react-native-reanimated` | ### Removed APIs | Removed | Replacement | | --------------------------- | -------------------------------------------- | | `useWorkletCallback` | `useCallback` + `'worklet'` directive | | `useAnimatedGestureHandler` | Gesture Handler 2 `Gesture` API | | `combineTransition` | `EntryExitTransition.entering(X).exiting(Y)` | | `addWhitelistedNativeProps` | Remove (no-op in v4) | | `addWhitelistedUIProps` | Remove (no-op in v4) | | `restDisplacementThreshold` | `energyThreshold` (or just remove) | | `restSpeedThreshold` | `energyThreshold` (or just remove) | ### withSpring Duration Change Actual completion time = perceptual `duration` x 1.5. To get equivalent timing from v3, divide duration by 1.5. --- ## API Quick Reference ### Core Hooks | Hook | Purpose | | ------------------------------- | ---------------------------------------------- | | `useSharedValue(initial)` | Create reactive value on UI thread | | `useAnimatedStyle(() => style)` | Derive animated styles from shared values | | `useAnimatedRef()` | Create ref for scroll offset tracking | | `useScrollOffset(ref)` | Track scroll position as shared value | | `useDerivedValue(() => expr)` | Compute derived value from shared values | | `useAnimatedProps(() => props)` | Animate non-style props (e.g., SVG attributes) | ### Animation Functions | Function | Purpose | Key Config | | ------------------------ | -------------- | -------------------------------------------------- | | `withTiming(to, config)` | Duration-based | `duration`, `easing` | | `withSpring(to, config)` | Physics spring | `damping`/`stiffness` or `duration`/`dampingRatio` | | `withDecay(config)` | Momentum | `velocity`, `clamp`, `rubberBandEffect` | ### Modifiers | Modifier | Purpose | | --------------------------------------- | ----------------------- | | `withDelay(ms, animation)` | Delay before starting | | `withSequence(...animations)` | Run animations in order | | `withRepeat(animation, count, reverse)` | Repeat animation | | `withClamp({ min, max }, animation)` | Clamp output range | | `cancelAnimation(sharedValue)` | Stop running animation | ### Extrapolation | Type | Behavior | | ------------------------ | ---------------------------- | | `Extrapolation.CLAMP` | Cap at output range edges | | `Extrapolation.EXTEND` | Extend linearly beyond range | | `Extrapolation.IDENTITY` | Return raw input value | ### Layout Animation Modifiers | Modifier | Purpose | | -------------------------------------------- | ----------------------------------- | | `.duration(ms)` | Set animation length | | `.delay(ms)` | Postpone start | | `.springify()` | Use spring physics | | `.damping(n)` / `.stiffness(n)` / `.mass(n)` | Spring config (after springify) | | `.withInitialValues(style)` | Override starting values | | `.withCallback(fn)` | Execute on completion | | `.reduceMotion(mode)` | Respect accessibility | | `.randomDelay()` | Random delay (0 to specified delay) | ### Animated Components | Built-in | Custom | | --------------------- | --------------------------------------------- | | `Animated.View` | `Animated.createAnimatedComponent(Component)` | | `Animated.Text` | (call at module level, not in render) | | `Animated.ScrollView` | | | `Animated.Image` | | | `Animated.FlatList` | | -
SKILL.md 18.8 KB
--- name: mobile-animation-reanimated description: React Native Reanimated 4 - shared values, animated styles, spring/timing/decay, layout animations, gesture integration, scroll-driven animations, interpolation, worklets, CSS animations --- # React Native Reanimated Patterns > **Quick Guide:** Reanimated 4 is New Architecture only (requires `react-native-worklets` as a separate dependency). Use `useSharedValue` + `useAnimatedStyle` for all animations. Animations run on the UI thread via worklets -- never block the JS thread. Use `withSpring` (physics-based) or `withTiming` (duration-based) for transitions, layout animations (`entering`/`exiting`) for mount/unmount, and `useScrollOffset` (renamed from `useScrollViewOffset`) for scroll-driven animations. Reanimated 4 also introduces CSS animations/transitions as a declarative alternative to the worklet API. --- <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 use Animated components (`Animated.View`, `Animated.Text`, etc.) for any animated styles -- passing animated styles to regular components causes errors)** **(You MUST keep static styles in `StyleSheet.create` and only animate dynamic properties in `useAnimatedStyle` -- animating static values wastes UI thread resources)** **(You MUST NOT mutate shared values inside `useAnimatedStyle` callbacks -- read only, or you cause infinite loops)** **(You MUST use `react-native-worklets` as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin`)** </critical_requirements> --- **Auto-detection:** Reanimated, react-native-reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, Animated.View, Animated.Text, Animated.ScrollView, entering, exiting, FadeIn, FadeOut, SlideIn, interpolate, interpolateColor, useScrollOffset, GestureDetector, Gesture.Pan, worklet, layout animation, shared value, energyThreshold, CSS animation, react-native-worklets **When to use:** - Animating view properties (opacity, transforms, colors) on the UI thread - Adding entering/exiting animations when components mount/unmount - Building gesture-driven animations (drag, swipe, pinch) - Creating scroll-driven header collapse, parallax, or sticky effects - Interpolating values across ranges (position to opacity, scroll to scale) - Implementing spring physics or timing-based transitions **When NOT to use:** - Simple boolean show/hide without animation (conditional rendering suffices) - Static layouts that never change at runtime - Animated.Value from React Native core (use Reanimated's shared values instead) **Key patterns covered:** - Shared values (`useSharedValue`) + animated styles (`useAnimatedStyle`) - Animation functions: `withTiming`, `withSpring`, `withDecay` - Layout animations: `entering`/`exiting` with predefined builders - Gesture integration: `Gesture.Pan` + shared values + `withDecay` - Scroll-driven animations with `useScrollOffset` - Interpolation: `interpolate` and `interpolateColor` - Worklet functions and the `'worklet'` directive - CSS animations and transitions (Reanimated 4 declarative API) **Detailed Resources:** - [examples/core.md](examples/core.md) - Shared values, animated styles, withTiming, withSpring, interpolation - [examples/layout-animations.md](examples/layout-animations.md) - Entering/exiting animations, predefined builders, custom layout animations - [examples/gestures.md](examples/gestures.md) - Pan gesture with shared values, swipe-to-dismiss, withDecay - [examples/scroll-animations.md](examples/scroll-animations.md) - Collapsing header, parallax, scroll-driven opacity - [reference.md](reference.md) - Decision frameworks, migration from 3.x, API quick reference --- <philosophy> ## Philosophy Reanimated runs animations on the **UI thread** via worklets, keeping the JS thread free for business logic. The core model: **shared values** are reactive state that bridges JS and UI threads, **animated styles** derive visual properties from shared values, and **animation functions** (`withSpring`, `withTiming`, `withDecay`) drive transitions between values. **Reanimated 4 key changes from 3.x:** - **New Architecture only** -- drops Legacy Architecture (bridge) support entirely - **`react-native-worklets`** as a separate package -- Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin` - **CSS animations/transitions** -- declarative API for state-driven animations (use CSS for simple state transitions, worklets for gesture/scroll-driven) - **`energyThreshold`** replaces `restDisplacementThreshold`/`restSpeedThreshold` in `withSpring` - **`useScrollOffset`** replaces `useScrollViewOffset` (deprecated alias remains) - **Threading functions moved** to `react-native-worklets`: `runOnJS` -> `scheduleOnRN`, `runOnUI` -> `scheduleOnUI` **When to use CSS animations vs worklets:** - **CSS animations/transitions** -- state-driven, declarative, less code, better optimizable by Reanimated - **Worklets** -- gesture-driven, scroll-driven, complex orchestration, frame-by-frame control **Backward compatibility:** All v2/v3 shared value and animation APIs work unchanged in v4. CSS animations and worklet-based animations work simultaneously and interchangeably. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Shared Values and Animated Styles The fundamental building blocks. `useSharedValue` creates reactive state on the UI thread. `useAnimatedStyle` derives styles that update when shared values change. ```typescript import Animated, { useSharedValue, useAnimatedStyle, withTiming, } from "react-native-reanimated"; const EXPANDED_HEIGHT = 200; const COLLAPSED_HEIGHT = 60; const ANIMATION_DURATION = 300; function CollapsibleCard() { const height = useSharedValue(COLLAPSED_HEIGHT); const animatedStyle = useAnimatedStyle(() => ({ height: height.value, })); const toggle = () => { height.value = withTiming( height.value === COLLAPSED_HEIGHT ? EXPANDED_HEIGHT : COLLAPSED_HEIGHT, { duration: ANIMATION_DURATION } ); }; return ( <Pressable onPress={toggle}> <Animated.View style={[styles.card, animatedStyle]}> <Text>Content</Text> </Animated.View> </Pressable> ); } ``` **Why good:** static styles stay in StyleSheet, only dynamic height in useAnimatedStyle, named constants for dimensions and durations, Animated.View receives the animated style **Key rules:** - Only animate dynamic properties in `useAnimatedStyle` -- static styles belong in `StyleSheet.create` - Never mutate shared values inside `useAnimatedStyle` -- it is read-only - Always apply animated styles to `Animated.*` components, not regular `View`/`Text` See [examples/core.md](examples/core.md) for complete examples including React Compiler compatibility (`get()`/`set()`). --- ### Pattern 2: withSpring and withTiming `withTiming` is duration-based (predictable timing). `withSpring` is physics-based (natural feel). Choose based on UX intent. ```typescript // Physics-based spring (natural, bouncy) sv.value = withSpring(TARGET, { damping: 100, stiffness: 800 }); // Duration-based spring (controlled timing with spring feel) sv.value = withSpring(TARGET, { duration: 500, dampingRatio: 0.8 }); // Timing with easing sv.value = withTiming(TARGET, { duration: ANIMATION_DURATION, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1), }); ``` **Reanimated 4 spring change:** `restDisplacementThreshold` and `restSpeedThreshold` are removed. Replaced by `energyThreshold` (relative to animation, default `6e-9`). In most cases, removing the old thresholds is sufficient -- no need to set `energyThreshold` manually. **Duration gotcha:** In v4, actual spring completion time = perceptual `duration` x 1.5. Divide existing duration values by 1.5 for equivalent behavior when migrating from v3. See [examples/core.md](examples/core.md) for spring config comparison and withDecay. --- ### Pattern 3: Layout Animations (Entering/Exiting) Predefined animations for component mount/unmount. Apply to `Animated.*` components via `entering` and `exiting` props. ```typescript import Animated, { FadeIn, FadeOutLeft } from "react-native-reanimated"; const ANIMATION_DURATION = 400; const ANIMATION_DELAY = 100; function NotificationBanner({ visible }: { visible: boolean }) { if (!visible) return null; return ( <Animated.View entering={FadeIn.duration(ANIMATION_DURATION).delay(ANIMATION_DELAY)} exiting={FadeOutLeft.duration(ANIMATION_DURATION)} style={styles.banner} > <Text>New notification</Text> </Animated.View> ); } ``` **Available builders:** `FadeIn`, `SlideInRight`, `ZoomIn`, `BounceIn`, `FlipInEasyX`, `LightSpeedInRight`, `RotateIn`, `PinwheelIn`, and all directional variants (Up/Down/Left/Right) plus corresponding `Out` variants. **Modifiers:** `.duration(ms)`, `.delay(ms)`, `.springify()` (with `.damping()`, `.stiffness()`, `.mass()`), `.withInitialValues()`, `.withCallback()`, `.reduceMotion()`. **Performance tip:** Define animation builders outside components or in `useMemo` -- creating them inline in render causes unnecessary object allocation. See [examples/layout-animations.md](examples/layout-animations.md) for custom builders, staggered lists, and `EntryExitTransition`. --- ### Pattern 4: Gesture Integration Reanimated integrates with `react-native-gesture-handler`. Gesture callbacks are **automatically workletized** -- you can access shared values directly without the `'worklet'` directive. ```typescript import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useSharedValue, useAnimatedStyle, withDecay, } from "react-native-reanimated"; function DraggableCard() { const offsetX = useSharedValue(0); const pan = Gesture.Pan() .onChange((e) => { offsetX.value += e.changeX; }) .onFinalize((e) => { offsetX.value = withDecay({ velocity: e.velocityX, rubberBandEffect: true }); }); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offsetX.value }], })); return ( <GestureDetector gesture={pan}> <Animated.View style={[styles.card, animatedStyle]} /> </GestureDetector> ); } ``` **Why good:** `onChange` gives delta values (not absolute), `onFinalize` adds momentum with `withDecay`, gesture callbacks access shared values directly on UI thread **Key points:** - Use `onChange` for incremental updates (delta), `onUpdate` for absolute position - `GestureHandlerRootView` must wrap your app near the root - This skill covers the Reanimated side of gesture animations -- for gesture configuration details (tap, pinch, fling, simultaneous gestures), see the gesture handler skill See [examples/gestures.md](examples/gestures.md) for swipe-to-dismiss, bottom sheet, and combined gestures. --- ### Pattern 5: Scroll-Driven Animations Use `useScrollOffset` to track scroll position as a shared value. Combine with `interpolate` for parallax, collapsing headers, and fade effects. ```typescript import Animated, { useAnimatedRef, useScrollOffset, useAnimatedStyle, interpolate, Extrapolation, } from "react-native-reanimated"; const HEADER_MAX = 200; const HEADER_MIN = 60; function CollapsibleHeader() { const scrollRef = useAnimatedRef<Animated.ScrollView>(); const scrollOffset = useScrollOffset(scrollRef); const headerStyle = useAnimatedStyle(() => ({ height: interpolate( scrollOffset.value, [0, HEADER_MAX - HEADER_MIN], [HEADER_MAX, HEADER_MIN], Extrapolation.CLAMP ), })); return ( <> <Animated.View style={[styles.header, headerStyle]} /> <Animated.ScrollView ref={scrollRef}> {/* content */} </Animated.ScrollView> </> ); } ``` **Why good:** `useScrollOffset` auto-detects horizontal/vertical, no manual scroll event handler needed, `Extrapolation.CLAMP` prevents values outside the range **Reanimated 4 rename:** `useScrollViewOffset` -> `useScrollOffset` (deprecated alias remains temporarily). See [examples/scroll-animations.md](examples/scroll-animations.md) for parallax, sticky elements, and scroll-to-hide tab bar. --- ### Pattern 6: Interpolation Map one value range to another. `interpolate` for numbers, `interpolateColor` for color transitions. ```typescript import { interpolate, interpolateColor, Extrapolation, } from "react-native-reanimated"; // Number interpolation: scroll position -> opacity const opacity = interpolate( scrollY.value, [0, 100], // input range [1, 0], // output range Extrapolation.CLAMP, ); // Color interpolation: progress -> background const backgroundColor = interpolateColor( progress.value, [0, 0.5, 1], // input range ["#FF0000", "#FFFF00", "#00FF00"], // output colors (red -> yellow -> green) ); ``` **Extrapolation options:** `CLAMP` (cap at edges), `EXTEND` (extrapolate linearly), `IDENTITY` (return input value). Can set left/right independently: `{ extrapolateLeft: Extrapolation.CLAMP, extrapolateRight: Extrapolation.EXTEND }`. **`interpolateColor` modes:** `'RGB'` (default) or `'HSV'`. HSV produces more perceptually uniform transitions for hue changes. See [examples/core.md](examples/core.md) for multi-step interpolation and color transition examples. --- ### Pattern 7: Worklet Functions Functions that run on the UI thread. Mark with `'worklet'` directive. Reanimated auto-workletizes callbacks in `useAnimatedStyle`, gesture handlers, and animation callbacks -- you only need explicit `'worklet'` for standalone helper functions. ```typescript import { scheduleOnRN } from "react-native-worklets"; function clampValue(value: number, min: number, max: number) { "worklet"; return Math.min(Math.max(value, min), max); } // Use in useAnimatedStyle -- auto-workletized, no directive needed const style = useAnimatedStyle(() => ({ opacity: clampValue(progress.value, 0, 1), })); ``` **Reanimated 4 threading changes:** | Reanimated 3 | Reanimated 4 (react-native-worklets) | | -------------------- | ------------------------------------ | | `runOnJS(fn)("arg")` | `scheduleOnRN(fn, "arg")` | | `runOnUI(fn)("arg")` | `scheduleOnUI(fn, "arg")` | **When you need explicit `'worklet'`:** - Standalone helper functions called from other worklets - Functions passed to `scheduleOnUI` **When you do NOT need it:** - `useAnimatedStyle` callbacks (auto-workletized) - Gesture handler callbacks (auto-workletized) - Animation callbacks (auto-workletized) --- ### Pattern 8: CSS Animations and Transitions (Reanimated 4) Declarative animation API modeled after web CSS. Best for state-driven animations. Worklet API remains for gesture/scroll-driven scenarios. ```typescript import Animated from "react-native-reanimated"; const TRANSITION_DURATION = 300; function ToggleBox({ expanded }: { expanded: boolean }) { return ( <Animated.View style={{ height: expanded ? 200 : 60, opacity: expanded ? 1 : 0.5, transitionProperty: "height, opacity", transitionDuration: `${TRANSITION_DURATION}ms`, transitionTimingFunction: "ease-in-out", }} /> ); } ``` **CSS animation keyframes:** ```typescript const PULSE_DURATION = 1000; <Animated.View style={{ animationName: { from: { transform: [{ scale: 1 }] }, to: { transform: [{ scale: 1.1 }] }, }, animationDuration: `${PULSE_DURATION}ms`, animationIterationCount: "infinite", animationDirection: "alternate", animationTimingFunction: "ease-in-out", }} /> ``` **When to use CSS vs worklets:** - CSS -- state-driven toggles, hover effects, simple transitions (less code, better optimizable) - Worklets -- gesture-driven, scroll-driven, frame-by-frame control, complex orchestration </patterns> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Passing animated styles to regular `View`/`Text` instead of `Animated.View`/`Animated.Text` -- causes silent failure or crash - Mutating shared values inside `useAnimatedStyle` -- causes infinite re-evaluation loops - Using `react-native-reanimated/plugin` in Babel config with Reanimated 4 -- must use `react-native-worklets/plugin` (and it must be last in the plugins array) - Using Reanimated 4.x with Legacy Architecture (old bridge) -- Reanimated 4 is New Architecture only - Animating static properties in `useAnimatedStyle` instead of keeping them in `StyleSheet` -- wastes UI thread resources - Using `restDisplacementThreshold`/`restSpeedThreshold` in `withSpring` -- removed in v4, replaced by `energyThreshold` **Medium Priority Issues:** - Creating layout animation builders inline in render -- allocates objects every render; define outside component or in `useMemo` - Using `runOnJS`/`runOnUI` instead of `scheduleOnRN`/`scheduleOnUI` -- old API moved to `react-native-worklets` - Missing `GestureHandlerRootView` at app root -- gestures silently fail without it - Using `useAnimatedGestureHandler` -- removed in v4, migrate to Gesture Handler 2's `Gesture` API **Gotchas and Edge Cases:** - `withSpring` duration: actual completion time = perceptual `duration` x 1.5 -- divide v3 duration values by 1.5 when migrating - `useScrollOffset` renamed from `useScrollViewOffset` -- deprecated alias still works temporarily - Shared value `.value` access is synchronous on UI thread but asynchronous on JS thread -- don't rely on immediate reads after writes on JS thread - `useWorkletCallback` removed -- replace with `useCallback` + `'worklet'` directive - React Compiler compatibility: use `sv.get()` and `sv.set()` instead of direct `.value` access when using React Compiler - `combineTransition` removed -- use `EntryExitTransition.entering(entering).exiting(exiting)` - Object shared values: reassign the entire object, never mutate individual properties -- mutations break reactivity tracking - Removing an animated style from a view does not unset the animated values -- explicitly set properties to `undefined` to reset - On New Architecture, layout animations use `nativeID` internally -- don't overwrite it on animated components </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST use Animated components (`Animated.View`, `Animated.Text`, etc.) for any animated styles -- passing animated styles to regular components causes errors)** **(You MUST keep static styles in `StyleSheet.create` and only animate dynamic properties in `useAnimatedStyle` -- animating static values wastes UI thread resources)** **(You MUST NOT mutate shared values inside `useAnimatedStyle` callbacks -- read only, or you cause infinite loops)** **(You MUST use `react-native-worklets` as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin`)** **Failure to follow these rules will cause animation failures, infinite loops, and crashes on the UI thread.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.