Skip to main content

Module 9: Animations and Gestures

React Native Reanimated

Build 60fps animations that run entirely on the UI thread with the modern animation library

🎯 Learning Objectives

  • Understand why Reanimated improves upon the built-in Animated API
  • Work with shared values and the worklet concept
  • Use useAnimatedStyle for dynamic styling
  • Implement animations with useSharedValue and withTiming/withSpring
  • Create animated reactions and derived values

Why Reanimated?

React Native Reanimated is a powerful animation library that runs animations directly on the UI thread, eliminating the performance bottlenecks of the JavaScript bridge. While the built-in Animated API is good for basic animations, Reanimated unlocks possibilities for complex, gesture-driven, and highly performant animations.

Animated API vs Reanimated

flowchart LR
    subgraph Animated["Built-in Animated API"]
        A1[JS Thread] -->|"Bridge (async)"| A2[UI Thread]
        A3[Limited native driver support]
        A4[Can't animate layout props natively]
    end
    
    subgraph Reanimated["Reanimated"]
        B1[Worklet Code] -->|"Runs directly"| B2[UI Thread]
        B3[Full native execution]
        B4[Animate any prop]
    end
    
    style Animated fill:#fff3e0
    style Reanimated fill:#e8f5e9

Key Advantages

Feature Animated API Reanimated
Transform animations βœ… Native driver βœ… UI thread
Layout animations ❌ JS thread only βœ… UI thread
Color animations ❌ JS thread only βœ… UI thread
Gesture integration ⚠️ Limited βœ… Seamless
Conditional logic ❌ Requires JS βœ… In worklets
Layout transitions ❌ Not supported βœ… Built-in

πŸ’‘ When to Use Reanimated

  • Complex gesture-driven animations (swipe cards, drag-to-delete)
  • Animations that respond to scroll position
  • Layout animations (width, height, position changes)
  • Any animation where 60fps is critical
  • Animations with conditional logic or calculations

Installation and Setup

Reanimated requires some configuration, but Expo makes it straightforward.

Installing with Expo

# Install Reanimated
npx expo install react-native-reanimated

# Reanimated is included in Expo SDK 49+ by default
# Just import and use!

Babel Configuration

Update your babel.config.js to include the Reanimated plugin:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // Reanimated plugin must be listed last
      'react-native-reanimated/plugin',
    ],
  };
};

⚠️ Important

After adding the Babel plugin, you need to clear the Metro bundler cache:

npx expo start --clear

Basic Import Pattern

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  withDelay,
  withSequence,
  withRepeat,
  Easing,
  interpolate,
  Extrapolation,
  runOnJS,
} from 'react-native-reanimated';

Shared Values

Shared values are the foundation of Reanimated. Unlike React state, shared values can be read and modified directly on the UI thread without causing re-renders.

Creating Shared Values

import { useSharedValue } from 'react-native-reanimated';

function MyComponent() {
  // Create a shared value with initial value
  const opacity = useSharedValue(0);
  const scale = useSharedValue(1);
  const position = useSharedValue({ x: 0, y: 0 });
  
  // Shared values persist across re-renders
  // but don't cause re-renders when modified
  
  // Direct modification (instant, no animation)
  const handleReset = () => {
    opacity.value = 0;
    scale.value = 1;
    position.value = { x: 0, y: 0 };
  };
  
  // Read the current value
  const logValue = () => {
    console.log('Current opacity:', opacity.value);
  };
  
  return (/* ... */);
}

Shared Value vs React State

import { useState } from 'react';
import { useSharedValue } from 'react-native-reanimated';

function ComparisonExample() {
  // React State - causes re-render on every change
  const [reactOpacity, setReactOpacity] = useState(0);
  
  // Shared Value - NO re-render on change
  const reanimatedOpacity = useSharedValue(0);
  
  // ❌ This causes 60 re-renders per second!
  const animateWithState = () => {
    const interval = setInterval(() => {
      setReactOpacity(prev => {
        if (prev >= 1) {
          clearInterval(interval);
          return 1;
        }
        return prev + 0.016; // ~60fps
      });
    }, 16);
  };
  
  // βœ… This runs on UI thread, no re-renders
  const animateWithReanimated = () => {
    reanimatedOpacity.value = withTiming(1, { duration: 1000 });
  };
  
  return (/* ... */);
}

Shared Value Types

import { useSharedValue } from 'react-native-reanimated';

// Numbers (most common)
const opacity = useSharedValue(0);
const rotation = useSharedValue(0);

// Objects
const position = useSharedValue({ x: 0, y: 0 });
const transform = useSharedValue({
  translateX: 0,
  translateY: 0,
  scale: 1,
  rotation: 0,
});

// Arrays
const path = useSharedValue([0, 0, 100, 100]);

// Strings (for colors, etc.)
const color = useSharedValue('#FF0000');

// Booleans
const isActive = useSharedValue(false);

// Accessing nested values
const updateX = () => {
  // Modify nested property
  position.value = {
    ...position.value,
    x: position.value.x + 10,
  };
};

Visualizing Shared Values

flowchart TD
    subgraph React["React Component (JS Thread)"]
        A[useSharedValue hook]
        B[Returns SharedValue object]
    end
    
    subgraph Shared["Shared Value"]
        C[".value property"]
        D[Accessible from both threads]
    end
    
    subgraph UI["UI Thread"]
        E[useAnimatedStyle]
        F[Gesture handlers]
        G[Worklets]
    end
    
    A --> B
    B --> C
    C --> D
    D --> E
    D --> F
    D --> G
    
    style React fill:#fff3e0
    style Shared fill:#e3f2fd
    style UI fill:#e8f5e9

Animated Styles

The useAnimatedStyle hook creates style objects that automatically update when shared values change, all on the UI thread.

Basic useAnimatedStyle

import React from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function AnimatedBox() {
  const scale = useSharedValue(1);
  
  // This function runs on the UI thread
  const animatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{ scale: scale.value }],
    };
  });
  
  const handlePress = () => {
    scale.value = withSpring(scale.value === 1 ? 1.5 : 1);
  };
  
  return (
    <View style={styles.container}>
      <Pressable onPress={handlePress}>
        <Animated.View style={[styles.box, animatedStyles]} />
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#007AFF',
    borderRadius: 10,
  },
});

Multiple Animated Properties

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  interpolateColor,
} from 'react-native-reanimated';

function MultiPropertyAnimation() {
  const progress = useSharedValue(0);
  
  const animatedStyles = useAnimatedStyle(() => {
    return {
      opacity: progress.value,
      transform: [
        { translateY: (1 - progress.value) * 50 },
        { scale: 0.8 + (progress.value * 0.2) },
        { rotate: `${progress.value * 360}deg` },
      ],
      backgroundColor: interpolateColor(
        progress.value,
        [0, 1],
        ['#FF0000', '#00FF00']
      ),
    };
  });
  
  const animate = () => {
    progress.value = withTiming(progress.value === 0 ? 1 : 0, {
      duration: 500,
    });
  };
  
  return (
    <Pressable onPress={animate}>
      <Animated.View style={[styles.box, animatedStyles]} />
    </Pressable>
  );
}

Conditional Styles in Worklets

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function ConditionalAnimation() {
  const isExpanded = useSharedValue(false);
  const position = useSharedValue(0);
  
  const animatedStyles = useAnimatedStyle(() => {
    // You can use conditionals inside useAnimatedStyle!
    const backgroundColor = isExpanded.value ? '#34C759' : '#007AFF';
    const borderRadius = isExpanded.value ? 20 : 10;
    
    return {
      width: isExpanded.value ? 200 : 100,
      height: isExpanded.value ? 200 : 100,
      backgroundColor,
      borderRadius,
      transform: [
        { translateX: position.value },
      ],
    };
  });
  
  const toggle = () => {
    isExpanded.value = !isExpanded.value;
    position.value = withSpring(isExpanded.value ? 50 : 0);
  };
  
  return (
    <Pressable onPress={toggle}>
      <Animated.View style={animatedStyles} />
    </Pressable>
  );
}

⚠️ useAnimatedStyle Rules

  • Must return a style object
  • Runs on UI thread (it's a worklet)
  • Don't call React hooks or access React state inside
  • Don't use console.log (use runOnJS instead)
  • Must be used with Animated.View or other Animated components

Animation Functions

Reanimated provides animation functions that define how values change over time. These replace the Animated.timing, Animated.spring, and Animated.decay from the built-in API.

withTiming

Creates a timing-based animation with configurable duration and easing.

import Animated, {
  useSharedValue,
  withTiming,
  Easing,
} from 'react-native-reanimated';

function TimingExample() {
  const opacity = useSharedValue(0);
  const translateX = useSharedValue(-100);
  
  const fadeIn = () => {
    // Basic timing animation
    opacity.value = withTiming(1, {
      duration: 500,
    });
  };
  
  const slideIn = () => {
    // Timing with custom easing
    translateX.value = withTiming(0, {
      duration: 300,
      easing: Easing.out(Easing.cubic),
    });
  };
  
  const animateWithCallback = () => {
    opacity.value = withTiming(1, { duration: 500 }, (finished) => {
      // Callback runs on UI thread
      if (finished) {
        // Animation completed
        console.log('Animation finished!'); // Won't work!
        // Use runOnJS for JS thread operations
      }
    });
  };
  
  return (/* ... */);
}

// Available Easing functions
const easingExamples = {
  linear: Easing.linear,
  ease: Easing.ease,
  
  // Quadratic
  easeIn: Easing.in(Easing.quad),
  easeOut: Easing.out(Easing.quad),
  easeInOut: Easing.inOut(Easing.quad),
  
  // Cubic
  cubicIn: Easing.in(Easing.cubic),
  cubicOut: Easing.out(Easing.cubic),
  
  // Exponential
  expIn: Easing.in(Easing.exp),
  expOut: Easing.out(Easing.exp),
  
  // Elastic and bounce
  elastic: Easing.elastic(1),
  bounce: Easing.bounce,
  
  // Back (overshoots)
  back: Easing.back(1.5),
  
  // Custom bezier curve
  bezier: Easing.bezier(0.25, 0.1, 0.25, 1),
};

withSpring

Creates physics-based spring animations. These feel more natural for interactive UI.

import Animated, {
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

function SpringExample() {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);
  
  // Default spring configuration
  const bounceDefault = () => {
    scale.value = withSpring(1.5);
  };
  
  // Custom spring configuration
  const bounceCustom = () => {
    scale.value = withSpring(1.5, {
      damping: 10,        // How quickly spring settles (default: 10)
      stiffness: 100,     // Spring stiffness (default: 100)
      mass: 1,            // Object mass (default: 1)
      overshootClamping: false,  // Prevent overshooting (default: false)
      restDisplacementThreshold: 0.01,  // When to stop (default: 0.01)
      restSpeedThreshold: 2,            // Speed threshold (default: 2)
    });
  };
  
  // Very bouncy spring
  const bouncySpring = () => {
    scale.value = withSpring(1.5, {
      damping: 4,
      stiffness: 80,
    });
  };
  
  // Stiff, quick spring
  const stiffSpring = () => {
    scale.value = withSpring(1.5, {
      damping: 20,
      stiffness: 200,
    });
  };
  
  // Spring with initial velocity
  const springWithVelocity = () => {
    rotation.value = withSpring(360, {
      velocity: 1000,  // Initial velocity
      damping: 15,
    });
  };
  
  return (/* ... */);
}

// Spring presets for common use cases
const SpringPresets = {
  gentle: { damping: 15, stiffness: 100 },
  bouncy: { damping: 5, stiffness: 80 },
  stiff: { damping: 20, stiffness: 200 },
  slow: { damping: 20, stiffness: 50 },
};

withDecay

Creates momentum-based animations that decelerate over time. Perfect for fling gestures.

import Animated, {
  useSharedValue,
  withDecay,
} from 'react-native-reanimated';

function DecayExample() {
  const translateX = useSharedValue(0);
  
  // Basic decay with velocity
  const fling = (velocity: number) => {
    translateX.value = withDecay({
      velocity: velocity,      // Initial velocity (required)
      deceleration: 0.998,     // Deceleration rate (default: 0.998)
    });
  };
  
  // Decay with boundaries (clamp)
  const flingWithBounds = (velocity: number) => {
    translateX.value = withDecay({
      velocity: velocity,
      deceleration: 0.998,
      clamp: [-200, 200],  // Stop at these boundaries
    });
  };
  
  // Decay with rubberband effect at boundaries
  const flingRubberband = (velocity: number) => {
    translateX.value = withDecay({
      velocity: velocity,
      rubberBandEffect: true,
      rubberBandFactor: 0.6,
      clamp: [-200, 200],
    });
  };
  
  return (/* ... */);
}

withDelay

Delays the start of an animation.

import Animated, {
  useSharedValue,
  withDelay,
  withTiming,
  withSpring,
} from 'react-native-reanimated';

function DelayExample() {
  const opacity = useSharedValue(0);
  const translateY = useSharedValue(50);
  
  const animateIn = () => {
    // Delay the fade in by 300ms
    opacity.value = withDelay(300, withTiming(1, { duration: 500 }));
    
    // Slide up immediately
    translateY.value = withSpring(0);
  };
  
  return (/* ... */);
}

withSequence

Runs animations one after another.

import Animated, {
  useSharedValue,
  withSequence,
  withTiming,
  withSpring,
} from 'react-native-reanimated';

function SequenceExample() {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);
  
  // Simple sequence
  const pulseAnimation = () => {
    scale.value = withSequence(
      withTiming(1.2, { duration: 150 }),
      withTiming(1, { duration: 150 })
    );
  };
  
  // Shake animation
  const shakeAnimation = () => {
    rotation.value = withSequence(
      withTiming(-10, { duration: 50 }),
      withTiming(10, { duration: 50 }),
      withTiming(-10, { duration: 50 }),
      withTiming(10, { duration: 50 }),
      withTiming(0, { duration: 50 })
    );
  };
  
  // Complex sequence
  const complexAnimation = () => {
    scale.value = withSequence(
      withTiming(0.8, { duration: 100 }),  // Press down
      withSpring(1.2),                      // Bounce up
      withSpring(1)                         // Settle
    );
  };
  
  return (/* ... */);
}

withRepeat

Repeats an animation a specified number of times or infinitely.

import Animated, {
  useSharedValue,
  withRepeat,
  withTiming,
  withSequence,
  Easing,
  cancelAnimation,
} from 'react-native-reanimated';

function RepeatExample() {
  const rotation = useSharedValue(0);
  const pulse = useSharedValue(1);
  const bounce = useSharedValue(0);
  
  // Infinite rotation
  const startSpinning = () => {
    rotation.value = withRepeat(
      withTiming(360, { 
        duration: 1000, 
        easing: Easing.linear 
      }),
      -1,  // -1 = infinite, positive number = that many times
      false // reverse: if true, alternates direction
    );
  };
  
  // Stop spinning
  const stopSpinning = () => {
    cancelAnimation(rotation);
  };
  
  // Pulsing animation (repeats with reverse)
  const startPulsing = () => {
    pulse.value = withRepeat(
      withTiming(1.2, { duration: 500 }),
      -1,   // Infinite
      true  // Reverse each iteration
    );
  };
  
  // Bounce animation (3 times)
  const bounceThreeTimes = () => {
    bounce.value = withRepeat(
      withSequence(
        withTiming(-20, { duration: 200 }),
        withTiming(0, { duration: 200 })
      ),
      3,     // Repeat 3 times
      false  // Don't reverse
    );
  };
  
  return (/* ... */);
}

Combining Animations

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  withDelay,
  withSequence,
  withRepeat,
} from 'react-native-reanimated';

function CombinedAnimations() {
  const opacity = useSharedValue(0);
  const translateY = useSharedValue(50);
  const scale = useSharedValue(0.5);
  
  const animateIn = () => {
    // Fade in
    opacity.value = withTiming(1, { duration: 300 });
    
    // Slide up with spring
    translateY.value = withSpring(0, {
      damping: 12,
      stiffness: 100,
    });
    
    // Scale up after a delay, then pulse
    scale.value = withDelay(
      200,
      withSequence(
        withSpring(1.1, { damping: 4 }),
        withSpring(1, { damping: 8 })
      )
    );
  };
  
  const animateOut = () => {
    opacity.value = withTiming(0, { duration: 200 });
    translateY.value = withTiming(50, { duration: 200 });
    scale.value = withTiming(0.5, { duration: 200 });
  };
  
  const animatedStyles = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [
      { translateY: translateY.value },
      { scale: scale.value },
    ],
  }));
  
  return (
    <Animated.View style={animatedStyles}>
      {/* Content */}
    </Animated.View>
  );
}

Animation Callbacks

import Animated, {
  useSharedValue,
  withTiming,
  runOnJS,
} from 'react-native-reanimated';

function CallbackExample() {
  const [isComplete, setIsComplete] = useState(false);
  const opacity = useSharedValue(0);
  
  const animateWithCallback = () => {
    opacity.value = withTiming(
      1,
      { duration: 500 },
      (finished) => {
        // This runs on UI thread!
        if (finished) {
          // To update React state, use runOnJS
          runOnJS(setIsComplete)(true);
          
          // Chain another animation
          opacity.value = withDelay(
            1000,
            withTiming(0, { duration: 500 })
          );
        }
      }
    );
  };
  
  return (/* ... */);
}

Understanding Worklets

Worklets are small JavaScript functions that run on the UI thread. They're the secret behind Reanimated's performanceβ€”by running directly on the UI thread, they avoid the JavaScript bridge entirely.

What is a Worklet?

flowchart LR
    subgraph JS["JS Thread"]
        A[React Components]
        B[State Updates]
        C[Event Handlers]
    end
    
    subgraph UI["UI Thread"]
        D[useAnimatedStyle - worklet]
        E[Gesture callbacks - worklet]
        F[Animation callbacks - worklet]
    end
    
    A -.->|"Babel transform"| D
    
    style JS fill:#fff3e0
    style UI fill:#e8f5e9

Implicit Worklets

Some functions are automatically treated as worklets:

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useDerivedValue,
  useAnimatedScrollHandler,
} from 'react-native-reanimated';

// useAnimatedStyle callback is implicitly a worklet
const animatedStyles = useAnimatedStyle(() => {
  // This runs on UI thread
  return {
    opacity: opacity.value,
  };
});

// useDerivedValue callback is implicitly a worklet
const derivedOpacity = useDerivedValue(() => {
  // This runs on UI thread
  return Math.min(opacity.value * 2, 1);
});

// Scroll handler callbacks are implicitly worklets
const scrollHandler = useAnimatedScrollHandler({
  onScroll: (event) => {
    // This runs on UI thread
    scrollY.value = event.contentOffset.y;
  },
});

Creating Explicit Worklets

import { useSharedValue, runOnUI } from 'react-native-reanimated';

// Define a worklet with the 'worklet' directive
function myWorklet(value: number) {
  'worklet';
  // This function runs on UI thread
  return value * 2;
}

// Use within animated style
const animatedStyles = useAnimatedStyle(() => {
  const doubled = myWorklet(opacity.value);
  return { opacity: doubled };
});

// Run a worklet from JS thread
function triggerFromJS() {
  runOnUI(myWorklet)(5);
}

Worklet Limitations

// ❌ CANNOT do in worklets:

// Access React state
const animatedStyles = useAnimatedStyle(() => {
  // return { opacity: reactState }; // ERROR!
});

// Call React hooks
const animatedStyles = useAnimatedStyle(() => {
  // const [state, setState] = useState(); // ERROR!
});

// Use console.log directly
const animatedStyles = useAnimatedStyle(() => {
  // console.log(opacity.value); // Won't work properly
});

// Access non-shared variables from closure
let regularVariable = 5;
const animatedStyles = useAnimatedStyle(() => {
  // return { opacity: regularVariable }; // May not work as expected
});


// βœ… CAN do in worklets:

// Access shared values
const opacity = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => {
  return { opacity: opacity.value }; // βœ…
});

// Use math and calculations
const animatedStyles = useAnimatedStyle(() => {
  const calculated = Math.sin(progress.value * Math.PI);
  return { opacity: calculated }; // βœ…
});

// Use conditionals
const animatedStyles = useAnimatedStyle(() => {
  return {
    backgroundColor: isActive.value ? 'green' : 'red', // βœ…
  };
});

// Call runOnJS for JS thread operations
const animatedStyles = useAnimatedStyle(() => {
  if (progress.value > 0.5) {
    runOnJS(myJSFunction)(); // βœ…
  }
  return { opacity: progress.value };
});

runOnJS - Bridging Back to JS

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  runOnJS,
} from 'react-native-reanimated';

function RunOnJSExample() {
  const [status, setStatus] = useState('idle');
  const progress = useSharedValue(0);
  
  // Regular JS function
  const updateStatus = (newStatus: string) => {
    setStatus(newStatus);
    console.log('Status updated:', newStatus);
  };
  
  const startAnimation = () => {
    setStatus('animating');
    
    progress.value = withTiming(1, { duration: 1000 }, (finished) => {
      // Inside callback, we're on UI thread
      if (finished) {
        // Use runOnJS to call JS function
        runOnJS(updateStatus)('complete');
      }
    });
  };
  
  const animatedStyles = useAnimatedStyle(() => {
    // Can also use runOnJS in animated styles
    if (progress.value === 1) {
      runOnJS(updateStatus)('reached end');
    }
    
    return {
      opacity: progress.value,
    };
  });
  
  return (/* ... */);
}

πŸ’‘ Worklet Best Practices

  • Keep worklets small and focused
  • Avoid complex logic that could slow down the UI thread
  • Use runOnJS sparinglyβ€”it crosses the bridge
  • Don't modify shared values from multiple worklets simultaneously
  • Remember: worklets can't access React state or hooks

Derived and Reactive Values

Reanimated provides hooks for creating values that automatically update based on other shared values. This enables reactive programming patterns on the UI thread.

useDerivedValue

Creates a shared value that's computed from other shared values. It automatically updates when its dependencies change.

import Animated, {
  useSharedValue,
  useDerivedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function DerivedValueExample() {
  const x = useSharedValue(0);
  const y = useSharedValue(0);
  
  // Derived value: computed from x and y
  const distance = useDerivedValue(() => {
    return Math.sqrt(x.value ** 2 + y.value ** 2);
  });
  
  // Another derived value using the first
  const normalizedDistance = useDerivedValue(() => {
    const maxDistance = 200;
    return Math.min(distance.value / maxDistance, 1);
  });
  
  // Use in animated style
  const animatedStyles = useAnimatedStyle(() => ({
    opacity: 1 - normalizedDistance.value,
    transform: [
      { scale: 1 - (normalizedDistance.value * 0.3) },
    ],
  }));
  
  const moveToPosition = (newX: number, newY: number) => {
    x.value = withSpring(newX);
    y.value = withSpring(newY);
  };
  
  return (
    <Animated.View style={animatedStyles}>
      {/* Content */}
    </Animated.View>
  );
}

Interpolation with useDerivedValue

import Animated, {
  useSharedValue,
  useDerivedValue,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

function InterpolationExample() {
  const scrollY = useSharedValue(0);
  
  // Interpolate scroll position to header height
  const headerHeight = useDerivedValue(() => {
    return interpolate(
      scrollY.value,
      [0, 100],           // Input range
      [200, 80],          // Output range
      Extrapolation.CLAMP // Don't go outside range
    );
  });
  
  // Interpolate to header opacity
  const headerOpacity = useDerivedValue(() => {
    return interpolate(
      scrollY.value,
      [0, 50, 100],
      [1, 0.5, 0],
      Extrapolation.CLAMP
    );
  });
  
  // Interpolate rotation (in degrees)
  const rotation = useDerivedValue(() => {
    return `${interpolate(
      scrollY.value,
      [0, 100],
      [0, 180]
    )}deg`;
  });
  
  const headerStyles = useAnimatedStyle(() => ({
    height: headerHeight.value,
    opacity: headerOpacity.value,
  }));
  
  const iconStyles = useAnimatedStyle(() => ({
    transform: [{ rotate: rotation.value }],
  }));
  
  return (/* ... */);
}

// Extrapolation options:
// Extrapolation.CLAMP - Stop at boundaries
// Extrapolation.EXTEND - Continue beyond boundaries (default)
// Extrapolation.IDENTITY - Return input value outside range

// You can also specify left and right separately:
interpolate(
  value,
  [0, 100],
  [0, 1],
  {
    extrapolateLeft: Extrapolation.CLAMP,
    extrapolateRight: Extrapolation.EXTEND,
  }
);

Color Interpolation

import Animated, {
  useSharedValue,
  useDerivedValue,
  useAnimatedStyle,
  interpolateColor,
  withTiming,
} from 'react-native-reanimated';

function ColorInterpolationExample() {
  const progress = useSharedValue(0);
  
  // Simple two-color interpolation
  const backgroundColor = useDerivedValue(() => {
    return interpolateColor(
      progress.value,
      [0, 1],
      ['#FF0000', '#00FF00']
    );
  });
  
  // Multi-stop color interpolation
  const gradientColor = useDerivedValue(() => {
    return interpolateColor(
      progress.value,
      [0, 0.25, 0.5, 0.75, 1],
      ['#FF0000', '#FF9500', '#FFCC00', '#34C759', '#007AFF']
    );
  });
  
  const animatedStyles = useAnimatedStyle(() => ({
    backgroundColor: backgroundColor.value,
  }));
  
  const toggleColor = () => {
    progress.value = withTiming(progress.value === 0 ? 1 : 0, {
      duration: 500,
    });
  };
  
  return (
    <Pressable onPress={toggleColor}>
      <Animated.View style={[styles.box, animatedStyles]} />
    </Pressable>
  );
}

// Color format options:
// Hex: '#FF0000'
// RGB: 'rgb(255, 0, 0)'
// RGBA: 'rgba(255, 0, 0, 0.5)'
// HSL: 'hsl(0, 100%, 50%)'
// Named: 'red', 'blue', etc.

useAnimatedReaction

Executes a side effect when a shared value changes. Useful for triggering actions based on animation progress.

import Animated, {
  useSharedValue,
  useAnimatedReaction,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';

function ReactionExample() {
  const [hasReachedEnd, setHasReachedEnd] = useState(false);
  const position = useSharedValue(0);
  const progress = useSharedValue(0);
  
  // React to position changes
  useAnimatedReaction(
    // First function: returns the value to track
    () => position.value,
    // Second function: called when tracked value changes
    (currentValue, previousValue) => {
      // Both values are available
      console.log(`Changed from ${previousValue} to ${currentValue}`);
      
      // Trigger another animation
      if (currentValue > 100 && previousValue <= 100) {
        progress.value = withSpring(1);
      }
    }
  );
  
  // React to threshold crossing
  useAnimatedReaction(
    () => progress.value > 0.9,
    (reachedEnd, previouslyReached) => {
      if (reachedEnd && !previouslyReached) {
        // Call JS function when threshold is crossed
        runOnJS(setHasReachedEnd)(true);
      }
    }
  );
  
  // React to combined values
  useAnimatedReaction(
    () => ({
      pos: position.value,
      prog: progress.value,
    }),
    (current, previous) => {
      // React to changes in either value
      if (current.pos !== previous?.pos) {
        // Position changed
      }
      if (current.prog !== previous?.prog) {
        // Progress changed
      }
    }
  );
  
  return (/* ... */);
}

Derived Value Patterns

import Animated, {
  useSharedValue,
  useDerivedValue,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

function DerivedPatterns() {
  const scrollY = useSharedValue(0);
  const isExpanded = useSharedValue(false);
  
  // Clamped value
  const clampedScroll = useDerivedValue(() => {
    return Math.max(0, Math.min(scrollY.value, 200));
  });
  
  // Normalized value (0 to 1)
  const normalizedScroll = useDerivedValue(() => {
    return clampedScroll.value / 200;
  });
  
  // Inverted value
  const invertedScroll = useDerivedValue(() => {
    return 1 - normalizedScroll.value;
  });
  
  // Boolean to number (for animations)
  const expandedProgress = useDerivedValue(() => {
    return isExpanded.value ? 1 : 0;
  });
  
  // Smoothed value (for gesture-driven animations)
  const smoothedScroll = useDerivedValue(() => {
    return withSpring(scrollY.value, { damping: 20 });
  });
  
  // Threshold-based value
  const isPastThreshold = useDerivedValue(() => {
    return scrollY.value > 100;
  });
  
  // Parallax calculations
  const parallax = useDerivedValue(() => ({
    slow: scrollY.value * 0.3,
    medium: scrollY.value * 0.6,
    fast: scrollY.value * 1.2,
  }));
  
  const backgroundStyles = useAnimatedStyle(() => ({
    transform: [{ translateY: parallax.value.slow }],
  }));
  
  const midgroundStyles = useAnimatedStyle(() => ({
    transform: [{ translateY: parallax.value.medium }],
  }));
  
  const foregroundStyles = useAnimatedStyle(() => ({
    transform: [{ translateY: parallax.value.fast }],
  }));
  
  return (/* ... */);
}

Animated Props

Beyond styles, Reanimated can animate component props directly using useAnimatedProps. This is useful for animating SVG elements, text values, and other non-style properties.

useAnimatedProps

import Animated, {
  useSharedValue,
  useAnimatedProps,
  withTiming,
} from 'react-native-reanimated';
import Svg, { Circle } from 'react-native-svg';

// Create animated version of Circle
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

function AnimatedSVGExample() {
  const radius = useSharedValue(50);
  const strokeWidth = useSharedValue(2);
  
  // Animate SVG props directly
  const animatedProps = useAnimatedProps(() => ({
    r: radius.value,
    strokeWidth: strokeWidth.value,
  }));
  
  const expand = () => {
    radius.value = withTiming(100, { duration: 500 });
    strokeWidth.value = withTiming(5, { duration: 500 });
  };
  
  const contract = () => {
    radius.value = withTiming(50, { duration: 500 });
    strokeWidth.value = withTiming(2, { duration: 500 });
  };
  
  return (
    <Svg height="250" width="250">
      <AnimatedCircle
        cx="125"
        cy="125"
        stroke="#007AFF"
        fill="transparent"
        animatedProps={animatedProps}
      />
    </Svg>
  );
}

Animated Text

import Animated, {
  useSharedValue,
  useAnimatedProps,
  useDerivedValue,
  withTiming,
} from 'react-native-reanimated';
import { TextInput, StyleSheet } from 'react-native';

// Use TextInput as a display-only text component for animation
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

function AnimatedCounter() {
  const count = useSharedValue(0);
  
  // Format the number as text
  const text = useDerivedValue(() => {
    return `${Math.round(count.value)}`;
  });
  
  // Animate the text prop
  const animatedProps = useAnimatedProps(() => ({
    text: text.value,
    defaultValue: text.value,
  }));
  
  const increment = () => {
    count.value = withTiming(count.value + 100, { duration: 500 });
  };
  
  return (
    <View>
      <AnimatedTextInput
        style={styles.counter}
        animatedProps={animatedProps}
        editable={false}
      />
      <Pressable onPress={increment}>
        <Text>Add 100</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  counter: {
    fontSize: 48,
    fontWeight: 'bold',
    textAlign: 'center',
  },
});

Progress Ring with Animated Props

import React from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedProps,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import Svg, { Circle } from 'react-native-svg';

const AnimatedCircle = Animated.createAnimatedComponent(Circle);

interface ProgressRingProps {
  size?: number;
  strokeWidth?: number;
  progress: Animated.SharedValue<number>;
}

function ProgressRing({ 
  size = 120, 
  strokeWidth = 10, 
  progress 
}: ProgressRingProps) {
  const radius = (size - strokeWidth) / 2;
  const circumference = 2 * Math.PI * radius;
  
  const animatedProps = useAnimatedProps(() => ({
    strokeDashoffset: circumference * (1 - progress.value),
  }));
  
  return (
    <Svg width={size} height={size}>
      {/* Background circle */}
      <Circle
        cx={size / 2}
        cy={size / 2}
        r={radius}
        stroke="#E0E0E0"
        strokeWidth={strokeWidth}
        fill="transparent"
      />
      
      {/* Animated progress circle */}
      <AnimatedCircle
        cx={size / 2}
        cy={size / 2}
        r={radius}
        stroke="#007AFF"
        strokeWidth={strokeWidth}
        fill="transparent"
        strokeDasharray={circumference}
        strokeLinecap="round"
        animatedProps={animatedProps}
        transform={`rotate(-90 ${size / 2} ${size / 2})`}
      />
    </Svg>
  );
}

// Usage
function ProgressDemo() {
  const progress = useSharedValue(0);
  
  const animate = () => {
    progress.value = withTiming(
      progress.value === 0 ? 1 : 0,
      { duration: 1000, easing: Easing.inOut(Easing.ease) }
    );
  };
  
  return (
    <View style={styles.container}>
      <ProgressRing progress={progress} size={150} />
      <Pressable style={styles.button} onPress={animate}>
        <Text style={styles.buttonText}>Toggle Progress</Text>
      </Pressable>
    </View>
  );
}

Animated ScrollView Props

import Animated, {
  useSharedValue,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

function AnimatedScrollExample() {
  const scrollY = useSharedValue(0);
  
  // Scroll handler (runs on UI thread)
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
    onBeginDrag: (event) => {
      console.log('Drag started');
    },
    onEndDrag: (event) => {
      console.log('Drag ended');
    },
    onMomentumBegin: (event) => {
      console.log('Momentum started');
    },
    onMomentumEnd: (event) => {
      console.log('Momentum ended');
    },
  });
  
  // Header animation based on scroll
  const headerStyles = useAnimatedStyle(() => {
    const height = interpolate(
      scrollY.value,
      [0, 100],
      [200, 80],
      Extrapolation.CLAMP
    );
    
    const opacity = interpolate(
      scrollY.value,
      [0, 100],
      [1, 0],
      Extrapolation.CLAMP
    );
    
    return {
      height,
      opacity,
    };
  });
  
  return (
    <View style={styles.container}>
      <Animated.View style={[styles.header, headerStyles]}>
        <Text>Collapsing Header</Text>
      </Animated.View>
      
      <Animated.ScrollView
        onScroll={scrollHandler}
        scrollEventThrottle={16}
      >
        {/* Scroll content */}
      </Animated.ScrollView>
    </View>
  );
}

πŸ’‘ Creating Animated Components

Use Animated.createAnimatedComponent() to make any component animatable:

import { Pressable, TextInput } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Animated from 'react-native-reanimated';

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);

Hands-On Exercises

Exercise 1: Animated Toggle Switch

Create a custom toggle switch with smooth animations using Reanimated.

Requirements:

  • Thumb slides smoothly between positions
  • Background color transitions between states
  • Spring animation for natural feel
  • Track isOn state and call onChange callback
Show Solution
import React, { useState } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolateColor,
  runOnJS,
} from 'react-native-reanimated';

interface ToggleSwitchProps {
  value: boolean;
  onValueChange: (value: boolean) => void;
  trackColors?: { on: string; off: string };
  thumbColor?: string;
}

function ToggleSwitch({
  value,
  onValueChange,
  trackColors = { on: '#34C759', off: '#E9E9EA' },
  thumbColor = '#FFFFFF',
}: ToggleSwitchProps) {
  const progress = useSharedValue(value ? 1 : 0);
  
  const handlePress = () => {
    const newValue = !value;
    progress.value = withSpring(newValue ? 1 : 0, {
      damping: 15,
      stiffness: 120,
    });
    onValueChange(newValue);
  };
  
  const trackStyles = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      progress.value,
      [0, 1],
      [trackColors.off, trackColors.on]
    ),
  }));
  
  const thumbStyles = useAnimatedStyle(() => ({
    transform: [
      { translateX: progress.value * 22 },
      { scale: withSpring(progress.value === 0.5 ? 1.1 : 1) },
    ],
  }));
  
  return (
    <Pressable onPress={handlePress}>
      <Animated.View style={[styles.track, trackStyles]}>
        <Animated.View 
          style={[
            styles.thumb, 
            { backgroundColor: thumbColor },
            thumbStyles,
          ]} 
        />
      </Animated.View>
    </Pressable>
  );
}

// Usage
function ToggleDemo() {
  const [isEnabled, setIsEnabled] = useState(false);
  
  return (
    <View style={styles.container}>
      <ToggleSwitch
        value={isEnabled}
        onValueChange={setIsEnabled}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  track: {
    width: 51,
    height: 31,
    borderRadius: 16,
    padding: 2,
  },
  thumb: {
    width: 27,
    height: 27,
    borderRadius: 14,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
  },
});

Exercise 2: Collapsing Header

Build a scroll-responsive header that shrinks as the user scrolls down.

Requirements:

  • Header shrinks from 200px to 80px as user scrolls
  • Title fades out as header collapses
  • Background opacity changes with scroll
  • Use useAnimatedScrollHandler for scroll tracking
Show Solution
import React from 'react';
import { View, Text, StyleSheet, StatusBar } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedScrollHandler,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

const HEADER_MAX_HEIGHT = 200;
const HEADER_MIN_HEIGHT = 80;
const SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;

function CollapsingHeader() {
  const scrollY = useSharedValue(0);
  
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
  });
  
  const headerStyles = useAnimatedStyle(() => {
    const height = interpolate(
      scrollY.value,
      [0, SCROLL_DISTANCE],
      [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
      Extrapolation.CLAMP
    );
    
    return { height };
  });
  
  const titleStyles = useAnimatedStyle(() => {
    const opacity = interpolate(
      scrollY.value,
      [0, SCROLL_DISTANCE / 2],
      [1, 0],
      Extrapolation.CLAMP
    );
    
    const translateY = interpolate(
      scrollY.value,
      [0, SCROLL_DISTANCE],
      [0, -20],
      Extrapolation.CLAMP
    );
    
    const scale = interpolate(
      scrollY.value,
      [0, SCROLL_DISTANCE],
      [1, 0.8],
      Extrapolation.CLAMP
    );
    
    return {
      opacity,
      transform: [{ translateY }, { scale }],
    };
  });
  
  const smallTitleStyles = useAnimatedStyle(() => {
    const opacity = interpolate(
      scrollY.value,
      [SCROLL_DISTANCE / 2, SCROLL_DISTANCE],
      [0, 1],
      Extrapolation.CLAMP
    );
    
    return { opacity };
  });
  
  const backgroundStyles = useAnimatedStyle(() => {
    const opacity = interpolate(
      scrollY.value,
      [0, SCROLL_DISTANCE],
      [0.3, 1],
      Extrapolation.CLAMP
    );
    
    return {
      backgroundColor: `rgba(0, 122, 255, ${opacity})`,
    };
  });
  
  // Generate dummy content
  const content = Array.from({ length: 30 }, (_, i) => (
    <View key={i} style={styles.item}>
      <Text style={styles.itemText}>Item {i + 1}</Text>
    </View>
  ));
  
  return (
    <View style={styles.container}>
      <StatusBar barStyle="light-content" />
      
      <Animated.View style={[styles.header, headerStyles, backgroundStyles]}>
        <Animated.Text style={[styles.title, titleStyles]}>
          Welcome Back
        </Animated.Text>
        <Animated.Text style={[styles.smallTitle, smallTitleStyles]}>
          Welcome
        </Animated.Text>
      </Animated.View>
      
      <Animated.ScrollView
        onScroll={scrollHandler}
        scrollEventThrottle={16}
        contentContainerStyle={{ 
          paddingTop: HEADER_MAX_HEIGHT,
          paddingBottom: 20,
        }}
      >
        {content}
      </Animated.ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  header: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    zIndex: 100,
    justifyContent: 'flex-end',
    paddingBottom: 16,
    paddingHorizontal: 20,
  },
  title: {
    color: 'white',
    fontSize: 34,
    fontWeight: 'bold',
  },
  smallTitle: {
    position: 'absolute',
    bottom: 16,
    left: 20,
    color: 'white',
    fontSize: 20,
    fontWeight: '600',
  },
  item: {
    backgroundColor: 'white',
    marginHorizontal: 16,
    marginTop: 12,
    padding: 20,
    borderRadius: 12,
  },
  itemText: {
    fontSize: 16,
  },
});

Exercise 3: Animated Counter

Create a number counter that smoothly animates between values.

Requirements:

  • Display animated number that counts up/down
  • Format numbers with commas (e.g., 1,234)
  • Buttons to increment/decrement by various amounts
  • Spring animation for value changes
Show Solution
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet, TextInput } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedProps,
  useDerivedValue,
  withSpring,
} from 'react-native-reanimated';

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

function formatNumber(num: number): string {
  return Math.round(num).toLocaleString();
}

function AnimatedCounter() {
  const count = useSharedValue(0);
  
  const formattedValue = useDerivedValue(() => {
    return formatNumber(count.value);
  });
  
  const animatedProps = useAnimatedProps(() => ({
    text: formattedValue.value,
    defaultValue: formattedValue.value,
  }));
  
  const increment = (amount: number) => {
    count.value = withSpring(count.value + amount, {
      damping: 15,
      stiffness: 100,
    });
  };
  
  const reset = () => {
    count.value = withSpring(0, {
      damping: 15,
      stiffness: 100,
    });
  };
  
  return (
    <View style={styles.container}>
      <View style={styles.counterDisplay}>
        <AnimatedTextInput
          style={styles.counterText}
          animatedProps={animatedProps}
          editable={false}
        />
      </View>
      
      <View style={styles.buttonRow}>
        <Pressable
          style={[styles.button, styles.decrementButton]}
          onPress={() => increment(-100)}
        >
          <Text style={styles.buttonText}>-100</Text>
        </Pressable>
        
        <Pressable
          style={[styles.button, styles.decrementButton]}
          onPress={() => increment(-10)}
        >
          <Text style={styles.buttonText}>-10</Text>
        </Pressable>
        
        <Pressable
          style={[styles.button, styles.decrementButton]}
          onPress={() => increment(-1)}
        >
          <Text style={styles.buttonText}>-1</Text>
        </Pressable>
        
        <Pressable
          style={[styles.button, styles.incrementButton]}
          onPress={() => increment(1)}
        >
          <Text style={styles.buttonText}>+1</Text>
        </Pressable>
        
        <Pressable
          style={[styles.button, styles.incrementButton]}
          onPress={() => increment(10)}
        >
          <Text style={styles.buttonText}>+10</Text>
        </Pressable>
        
        <Pressable
          style={[styles.button, styles.incrementButton]}
          onPress={() => increment(100)}
        >
          <Text style={styles.buttonText}>+100</Text>
        </Pressable>
      </View>
      
      <Pressable style={styles.resetButton} onPress={reset}>
        <Text style={styles.resetText}>Reset</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  counterDisplay: {
    backgroundColor: '#f0f0f0',
    paddingHorizontal: 40,
    paddingVertical: 20,
    borderRadius: 16,
    marginBottom: 40,
  },
  counterText: {
    fontSize: 48,
    fontWeight: 'bold',
    textAlign: 'center',
    color: '#333',
    minWidth: 200,
  },
  buttonRow: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'center',
    gap: 10,
  },
  button: {
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 8,
    minWidth: 60,
    alignItems: 'center',
  },
  incrementButton: {
    backgroundColor: '#34C759',
  },
  decrementButton: {
    backgroundColor: '#FF3B30',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  resetButton: {
    marginTop: 30,
    paddingHorizontal: 30,
    paddingVertical: 12,
    backgroundColor: '#007AFF',
    borderRadius: 8,
  },
  resetText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

Summary

React Native Reanimated revolutionizes animations by running them entirely on the UI thread. This eliminates the performance bottleneck of the JavaScript bridge and enables silky-smooth 60fps animations.

🎯 Key Takeaways

  • Shared Values: Use useSharedValue for values that animate without causing re-renders
  • Animated Styles: Use useAnimatedStyle to create styles that update on the UI thread
  • Animation Functions:
    • withTiming β€” Duration-based animations with easing
    • withSpring β€” Physics-based spring animations
    • withDecay β€” Momentum-based deceleration
    • withDelay, withSequence, withRepeat β€” Composition
  • Worklets: Functions that run on UI thread; can use math and conditionals but not React state
  • runOnJS: Bridge back to JS thread for state updates and console logs
  • Derived Values: Use useDerivedValue for computed values that auto-update
  • Animated Props: Use useAnimatedProps for non-style properties like SVG attributes

Reanimated vs Animated API Quick Reference

Concept Animated API Reanimated
Creating values useRef(new Animated.Value(0)) useSharedValue(0)
Timing animation Animated.timing(value, config).start() value.value = withTiming(target, config)
Spring animation Animated.spring(value, config).start() value.value = withSpring(target, config)
Interpolation value.interpolate({ inputRange, outputRange }) interpolate(value, inputRange, outputRange)
Applying styles style={{ opacity: value }} useAnimatedStyle(() => ({ ... }))

In the next lesson, we'll explore common animation patterns and build practical, reusable animations that you can apply throughout your apps.