# Animations Reference Reanimated 3 animations, gestures, and transitions for Expo/React Native. ## Core Rules - **Only animate `transform` and `opacity`** — GPU-composited, no layout recalculation - Use `useDerivedValue` for computed animated values, not inline JS expressions - Use `Gesture.Tap` instead of `Pressable` inside `GestureDetector` - All Reanimated callbacks run as worklets on the UI thread — no async/await ## Setup ```bash npx expo install react-native-reanimated react-native-gesture-handler ``` ```js // babel.config.js module.exports = { presets: ["babel-preset-expo"], plugins: ["react-native-reanimated/plugin"] }; ``` ```tsx // app/_layout.tsx — wrap root in GestureHandlerRootView import { GestureHandlerRootView } from "react-native-gesture-handler"; export default function RootLayout() { return ; } ``` ## Entering / Exiting Animations ```tsx import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft, ZoomIn, ZoomOut, BounceIn, } from "react-native-reanimated"; // Basic Content // With options // Spring-based ``` ### Built-in Presets | Category | Entering | Exiting | |----------|----------|---------| | Fade | `FadeIn`, `FadeInUp`, `FadeInDown`, `FadeInLeft`, `FadeInRight` | `FadeOut*` | | Slide | `SlideInUp`, `SlideInDown`, `SlideInLeft`, `SlideInRight` | `SlideOut*` | | Zoom | `ZoomIn`, `ZoomInUp`, `ZoomInDown` | `ZoomOut*` | | Bounce | `BounceIn`, `BounceInUp`, `BounceInDown` | `BounceOut*` | | Flip | `FlipInXUp`, `FlipInYLeft` | `FlipOut*` | | Roll | `RollInLeft`, `RollInRight` | `RollOut*` | | Stretch | `StretchInX`, `StretchInY` | `StretchOut*` | | Pinwheel | `PinwheelIn` | `PinwheelOut` | | Rotate | `RotateInDownLeft` | `RotateOut*` | | LightSpeed | `LightSpeedInLeft` | `LightSpeedOut*` | ## Shared Values & useAnimatedStyle ```tsx import { useSharedValue, useAnimatedStyle, withSpring, withTiming, withRepeat, withSequence, Easing, } from "react-native-reanimated"; const offset = useSharedValue(0); const opacity = useSharedValue(1); const animStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }], opacity: opacity.value, })); // Animate offset.value = withSpring(100); opacity.value = withTiming(0, { duration: 300, easing: Easing.out(Easing.quad) }); // Repeat opacity.value = withRepeat(withTiming(0.3, { duration: 800 }), -1, true); // Sequence offset.value = withSequence( withTiming(-10, { duration: 100 }), withTiming(10, { duration: 100 }), withSpring(0), ); ``` ## useDerivedValue ```tsx import { useDerivedValue } from "react-native-reanimated"; const progress = useSharedValue(0); // 0–1 const rotation = useDerivedValue(() => `${progress.value * 360}deg`); const scale = useDerivedValue(() => 0.5 + progress.value * 0.5); const animStyle = useAnimatedStyle(() => ({ transform: [{ rotate: rotation.value }, { scale: scale.value }], })); ``` ## Layout Animations ```tsx import { Layout, LinearTransition, CurvedTransition } from "react-native-reanimated"; // Item reorder/add/remove animation {/* Content that changes size/position */} // Spring layout transition ``` ## Gestures ```tsx import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated"; // Pan gesture const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); const panGesture = Gesture.Pan() .onUpdate((e) => { offsetX.value = e.translationX; offsetY.value = e.translationY; }) .onEnd(() => { offsetX.value = withSpring(0); offsetY.value = withSpring(0); }); const animStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offsetX.value }, { translateY: offsetY.value }], })); ``` ```tsx // Tap gesture (use instead of Pressable inside GestureDetector) const tapGesture = Gesture.Tap() .numberOfTaps(1) .onEnd(() => { scale.value = withSequence(withTiming(0.95), withSpring(1)); }); // Pinch gesture const baseScale = useSharedValue(1); const savedScale = useSharedValue(1); const pinchGesture = Gesture.Pinch() .onUpdate((e) => { baseScale.value = savedScale.value * e.scale; }) .onEnd(() => { savedScale.value = baseScale.value; }); // Composed gestures const composed = Gesture.Simultaneous(panGesture, pinchGesture); const exclusive = Gesture.Exclusive(tapGesture, panGesture); ``` ## Scroll-Driven Animations ```tsx import Animated, { useAnimatedScrollHandler, useSharedValue, interpolate, Extrapolation, } from "react-native-reanimated"; const scrollY = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler((e) => { scrollY.value = e.contentOffset.y; }); // Parallax header const headerStyle = useAnimatedStyle(() => ({ transform: [{ translateY: interpolate(scrollY.value, [0, 200], [0, -100], Extrapolation.CLAMP), }], opacity: interpolate(scrollY.value, [0, 200], [1, 0], Extrapolation.CLAMP), })); Parallax Header ``` ## Zoom Transitions (Expo Router, iOS 18+) ```tsx import { Link } from "expo-router"; ``` ## Adding Animations to State Changes ```tsx // ✓ Always add entering/exiting for state-driven UI changes {isVisible && ( )} // ✓ AnimatedFlatList for list item changes import Animated from "react-native-reanimated"; const AnimatedFlashList = Animated.createAnimatedComponent(FlashList); ``` ## Common Mistakes | Wrong | Right | |-------|-------| | Animate `width`/`height` | Animate `transform: scaleX/scaleY` | | Inline JS math in `useAnimatedStyle` | `useDerivedValue` for computations | | `Pressable` inside `GestureDetector` | `Gesture.Tap()` | | `async` in worklet | Run async outside, update sharedValue in callback | | Frequent `console.log` in worklet | `console.log` works but serializes to JS thread — use sparingly in hot paths |