Module 4: StyleSheet Deep Dive

Animation Basics

Bring your UI to life with smooth, performant animations

Table of Contents

🎯 Learning Objectives

  • Understand the Animated API architecture and why it's performant
  • Create and manage Animated.Value for animatable properties
  • Use timing, spring, and decay animations appropriately
  • Transform animated values with interpolation
  • Combine multiple animations with sequence, parallel, and stagger
  • Implement common animation patterns: fade, slide, scale, rotate
  • Optimize animations for 60fps performance

Introduction

Animation transforms static interfaces into dynamic, engaging experiences. A well-placed animation can guide attention, provide feedback, create delight, and make your app feel polished and professional.

React Native's Animated API is designed from the ground up for performance. Unlike web animations that often cause layout thrashing, React Native animations can run entirely on the native UI thread, achieving smooth 60fps even on lower-end devices.

✨ Why Animation Matters

Users perceive animated interfaces as faster, more responsive, and more premium. A 200ms fade transition feels instant, while an abrupt change feels jarring. Animation isn't decoration — it's communication.

Animation Options in React Native

API Best For Complexity
Animated API Most animations, this lesson's focus Medium
LayoutAnimation Automatic layout transitions Low
Reanimated Complex gestures, worklets High

This lesson focuses on the built-in Animated API. It's powerful enough for most use cases and requires no additional dependencies. We'll cover Reanimated in a later module for advanced scenarios.

flowchart LR
    subgraph JS["JavaScript Thread"]
        AV["Animated.Value"]
        Config["Animation Config"]
    end
    
    subgraph Native["Native UI Thread"]
        Driver["Native Driver"]
        UI["UI Updates"]
    end
    
    AV --> Config
    Config -->|"useNativeDriver: true"| Driver
    Driver --> UI
    
    style JS fill:#e3f2fd
    style Native fill:#e8f5e9
                

The Animated API

The Animated API consists of three main parts: animated values that store animation state, animation functions that change those values over time, and animated components that render the values.

Core Concepts

import { Animated } from 'react-native';

// 1. Animated Value - stores the animation state
const opacity = new Animated.Value(0);

// 2. Animation Function - changes the value over time
Animated.timing(opacity, {
  toValue: 1,
  duration: 300,
  useNativeDriver: true,
}).start();

// 3. Animated Component - renders the animated value
<Animated.View style={{ opacity }}>
  <Text>I fade in!</Text>
</Animated.View>

Animated Components

React Native provides pre-wrapped animated versions of common components:

// Built-in animated components
Animated.View      // Most common, for containers
Animated.Text      // For text animations
Animated.Image     // For image animations
Animated.ScrollView // For scroll-linked animations
Animated.FlatList  // For list animations

// Create custom animated components
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

What Can Be Animated?

Not all style properties support animation. Here's what works:

✅ Native Driver Compatible

  • opacity
  • transform (translateX/Y, scale, rotate)

Run on UI thread, best performance

⚠️ JS Thread Only

  • width, height
  • backgroundColor
  • margin, padding
  • borderRadius

Require useNativeDriver: false

✅ Best Practice: Use Native Driver

Always use useNativeDriver: true when animating opacity or transform. This runs the animation entirely on the native thread, ensuring smooth 60fps regardless of JavaScript thread activity.

Animated.Value

Animated.Value is the foundation of all animations. It holds a single numeric value that can change over time.

Creating Animated Values

import { useRef } from 'react';
import { Animated } from 'react-native';

function MyComponent() {
  // Create animated value with useRef to persist across renders
  const fadeAnim = useRef(new Animated.Value(0)).current;
  const slideAnim = useRef(new Animated.Value(-100)).current;
  const scaleAnim = useRef(new Animated.Value(0.5)).current;
  
  // Initial values represent starting state:
  // fadeAnim: 0 = fully transparent
  // slideAnim: -100 = 100 units off-screen left
  // scaleAnim: 0.5 = half size
}

Reading and Setting Values

const animValue = useRef(new Animated.Value(0)).current;

// Set value immediately (no animation)
animValue.setValue(100);

// Read current value (async, for debugging)
animValue.addListener(({ value }) => {
  console.log('Current value:', value);
});

// Remove listener when done
animValue.removeAllListeners();

Animated.ValueXY for 2D

For animations involving both X and Y coordinates (like drag gestures), use Animated.ValueXY:

const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;

// Animate to new position
Animated.spring(position, {
  toValue: { x: 100, y: 200 },
  useNativeDriver: true,
}).start();

// Use in styles
<Animated.View
  style={{
    transform: position.getTranslateTransform(),
    // Equivalent to:
    // transform: [
    //   { translateX: position.x },
    //   { translateY: position.y },
    // ],
  }}
/>

Using Animated Values in Styles

function FadeInView({ children }) {
  const opacity = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 500,
      useNativeDriver: true,
    }).start();
  }, []);
  
  return (
    <Animated.View style={{ opacity }}>
      {children}
    </Animated.View>
  );
}

// For transform animations
function SlideInView({ children }) {
  const translateX = useRef(new Animated.Value(-300)).current;
  
  useEffect(() => {
    Animated.spring(translateX, {
      toValue: 0,
      useNativeDriver: true,
    }).start();
  }, []);
  
  return (
    <Animated.View 
      style={{
        transform: [{ translateX }],
      }}
    >
      {children}
    </Animated.View>
  );
}

Animation Types

React Native provides three animation functions, each with different characteristics.

Animated.timing()

The most common animation type. Animates a value over a specified duration using an easing function.

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

Animated.timing(animatedValue, {
  toValue: 1,                    // Target value
  duration: 300,                 // Duration in milliseconds
  delay: 0,                      // Delay before starting
  easing: Easing.ease,           // Easing function
  useNativeDriver: true,         // Use native driver
}).start((finished) => {
  // Callback when animation completes
  if (finished) {
    console.log('Animation finished!');
  }
});

Easing Functions

import { Easing } from 'react-native';

// Built-in easing functions
Easing.linear        // Constant speed
Easing.ease          // Gentle acceleration and deceleration
Easing.quad          // Quadratic (power of 2)
Easing.cubic         // Cubic (power of 3)
Easing.sin           // Sinusoidal
Easing.exp           // Exponential
Easing.circle        // Circular
Easing.bounce        // Bouncy ending

// Modifiers
Easing.in(Easing.quad)     // Accelerate
Easing.out(Easing.quad)    // Decelerate
Easing.inOut(Easing.quad)  // Both

// Bezier curves (like CSS)
Easing.bezier(0.25, 0.1, 0.25, 1)  // Custom curve
linear ease easeIn easeOut X = time, Y = progress

Animated.spring()

Creates natural, physics-based animations. Great for elements that should feel like they have mass.

Animated.spring(animatedValue, {
  toValue: 1,
  
  // Physics configuration (choose one approach):
  
  // Approach 1: Bounciness and Speed
  friction: 7,        // Controls bounce (lower = more bouncy)
  tension: 40,        // Controls speed (higher = faster)
  
  // Approach 2: Spring physics
  // stiffness: 100,   // Spring stiffness
  // damping: 10,      // Friction force
  // mass: 1,          // Object mass
  
  // Approach 3: Timing-based
  // speed: 12,        // Animation speed
  // bounciness: 8,    // Bounce amount
  
  useNativeDriver: true,
}).start();

💡 When to Use Spring

  • Button press feedback
  • Modal/sheet presentations
  • Drag and release
  • Pull to refresh
  • Anything that should feel "physical"

Animated.decay()

Animates a value from an initial velocity, gradually slowing to a stop. Perfect for scroll-like momentum.

Animated.decay(animatedValue, {
  velocity: 0.5,           // Initial velocity
  deceleration: 0.997,     // Rate of slowdown (0.997 is default)
  useNativeDriver: true,
}).start();

Comparison

Type Use Case Duration
timing Precise, timed animations Fixed (you specify)
spring Natural, physical feel Dynamic (physics-based)
decay Momentum/inertia Dynamic (velocity-based)

Interpolation

Interpolation transforms an animated value from one range to another. It's incredibly powerful for creating complex animations from a single animated value.

Basic Interpolation

const animValue = useRef(new Animated.Value(0)).current;

// Map 0-1 to 0-100
const translateX = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100],
});

// As animValue goes 0 → 1, translateX goes 0 → 100

Multiple Segments

const progress = useRef(new Animated.Value(0)).current;

// Create a multi-stage animation
const opacity = progress.interpolate({
  inputRange: [0, 0.5, 1],
  outputRange: [0, 1, 0],  // Fade in, then fade out
});

// Scale up quickly, then slowly
const scale = progress.interpolate({
  inputRange: [0, 0.3, 1],
  outputRange: [0, 1, 1.2],
});

// Animate progress from 0 to 1
Animated.timing(progress, {
  toValue: 1,
  duration: 1000,
  useNativeDriver: true,
}).start();
Interpolation: One Value → Multiple Outputs progress: 0→1 opacity: 0→1→0 scale: 0→1→1.2 rotate: 0→360deg Single animated value drives multiple style properties

String Output Values

Interpolation can output strings for properties like colors and rotation:

const animValue = useRef(new Animated.Value(0)).current;

// Rotation (degrees)
const rotate = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '360deg'],
});

// Colors
const backgroundColor = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: ['#ff0000', '#00ff00'],
});
// Note: Color interpolation requires useNativeDriver: false

// Usage
<Animated.View
  style={{
    transform: [{ rotate }],
    backgroundColor, // Only with useNativeDriver: false
  }}
/>

Extrapolation

Control what happens when the input value goes outside the defined range:

const animValue = useRef(new Animated.Value(0)).current;

const clamped = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100],
  extrapolate: 'clamp',  // Stay at boundaries
});

const extended = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100],
  extrapolate: 'extend', // Continue beyond (default)
});

// If animValue = 1.5:
// clamped = 100 (stopped at max)
// extended = 150 (continued past max)

// Mixed extrapolation
const mixed = animValue.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100],
  extrapolateLeft: 'clamp',   // Clamp below 0
  extrapolateRight: 'extend', // Extend above 1
});

Combining Animations

Complex animations often require multiple animated values working together. React Native provides composition functions to orchestrate them.

Animated.parallel()

Runs multiple animations simultaneously. All animations start at the same time.

const opacity = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(50)).current;
const scale = useRef(new Animated.Value(0.8)).current;

function fadeInUp() {
  Animated.parallel([
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }),
    Animated.timing(translateY, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true,
    }),
    Animated.spring(scale, {
      toValue: 1,
      useNativeDriver: true,
    }),
  ]).start();
}

// Usage
<Animated.View
  style={{
    opacity,
    transform: [{ translateY }, { scale }],
  }}
>
  <Content />
</Animated.View>

Animated.sequence()

Runs animations one after another. Each animation starts when the previous one finishes.

const step1 = useRef(new Animated.Value(0)).current;
const step2 = useRef(new Animated.Value(0)).current;
const step3 = useRef(new Animated.Value(0)).current;

function runSequence() {
  Animated.sequence([
    // First: fade in
    Animated.timing(step1, {
      toValue: 1,
      duration: 200,
      useNativeDriver: true,
    }),
    // Then: slide
    Animated.timing(step2, {
      toValue: 1,
      duration: 200,
      useNativeDriver: true,
    }),
    // Finally: scale
    Animated.spring(step3, {
      toValue: 1,
      useNativeDriver: true,
    }),
  ]).start();
}

Animated.stagger()

Starts animations in parallel but with a delay between each start. Perfect for list item animations.

function StaggeredList({ items }) {
  // Create an animated value for each item
  const animValues = useRef(
    items.map(() => new Animated.Value(0))
  ).current;
  
  useEffect(() => {
    // Stagger animations with 100ms delay between each
    Animated.stagger(
      100, // Delay between each animation start
      animValues.map(anim =>
        Animated.spring(anim, {
          toValue: 1,
          useNativeDriver: true,
        })
      )
    ).start();
  }, []);
  
  return (
    <View>
      {items.map((item, index) => (
        <Animated.View
          key={item.id}
          style={{
            opacity: animValues[index],
            transform: [{
              translateX: animValues[index].interpolate({
                inputRange: [0, 1],
                outputRange: [-50, 0],
              }),
            }],
          }}
        >
          <ItemCard item={item} />
        </Animated.View>
      ))}
    </View>
  );
}
parallel() All start together sequence() One after another stagger(100) Offset starts by delay Time →

Animated.loop()

Repeats an animation continuously. Great for loading indicators and attention-grabbing animations.

const rotation = useRef(new Animated.Value(0)).current;

useEffect(() => {
  const animation = Animated.loop(
    Animated.timing(rotation, {
      toValue: 1,
      duration: 1000,
      easing: Easing.linear,
      useNativeDriver: true,
    })
  );
  
  animation.start();
  
  // Cleanup
  return () => animation.stop();
}, []);

const rotate = rotation.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '360deg'],
});

// Spinning loader
<Animated.View style={{ transform: [{ rotate }] }}>
  <LoaderIcon />
</Animated.View>

Loop with Iterations

// Loop a specific number of times
Animated.loop(
  Animated.sequence([
    Animated.timing(scale, {
      toValue: 1.2,
      duration: 200,
      useNativeDriver: true,
    }),
    Animated.timing(scale, {
      toValue: 1,
      duration: 200,
      useNativeDriver: true,
    }),
  ]),
  { iterations: 3 } // Bounce 3 times, then stop
).start();

Combining Parallel and Sequence

// Complex animation: slide in while fading, then pulse
Animated.sequence([
  // Phase 1: Enter
  Animated.parallel([
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }),
    Animated.spring(translateY, {
      toValue: 0,
      useNativeDriver: true,
    }),
  ]),
  // Phase 2: Attention pulse
  Animated.sequence([
    Animated.timing(scale, {
      toValue: 1.1,
      duration: 150,
      useNativeDriver: true,
    }),
    Animated.timing(scale, {
      toValue: 1,
      duration: 150,
      useNativeDriver: true,
    }),
  ]),
]).start();

Common Animation Patterns

Here are production-ready implementations of the most common animations.

Pattern: Fade In

function FadeIn({ 
  children, 
  duration = 300, 
  delay = 0 
}: { 
  children: ReactNode; 
  duration?: number; 
  delay?: number;
}) {
  const opacity = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    Animated.timing(opacity, {
      toValue: 1,
      duration,
      delay,
      useNativeDriver: true,
    }).start();
  }, []);
  
  return (
    <Animated.View style={{ opacity }}>
      {children}
    </Animated.View>
  );
}

Pattern: Slide In

type Direction = 'left' | 'right' | 'up' | 'down';

function SlideIn({ 
  children, 
  direction = 'up',
  distance = 50,
}: { 
  children: ReactNode; 
  direction?: Direction;
  distance?: number;
}) {
  const animValue = useRef(new Animated.Value(0)).current;
  
  const getTransform = () => {
    const axis = direction === 'left' || direction === 'right' 
      ? 'translateX' 
      : 'translateY';
    const start = direction === 'right' || direction === 'down' 
      ? distance 
      : -distance;
    
    return {
      [axis]: animValue.interpolate({
        inputRange: [0, 1],
        outputRange: [start, 0],
      }),
    };
  };
  
  useEffect(() => {
    Animated.spring(animValue, {
      toValue: 1,
      useNativeDriver: true,
    }).start();
  }, []);
  
  return (
    <Animated.View
      style={{
        opacity: animValue,
        transform: [getTransform()],
      }}
    >
      {children}
    </Animated.View>
  );
}

Pattern: Scale Press Feedback

function ScaleButton({ onPress, children }) {
  const scale = useRef(new Animated.Value(1)).current;
  
  const handlePressIn = () => {
    Animated.spring(scale, {
      toValue: 0.95,
      useNativeDriver: true,
    }).start();
  };
  
  const handlePressOut = () => {
    Animated.spring(scale, {
      toValue: 1,
      friction: 3,
      tension: 40,
      useNativeDriver: true,
    }).start();
  };
  
  return (
    <Pressable
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      onPress={onPress}
    >
      <Animated.View style={{ transform: [{ scale }] }}>
        {children}
      </Animated.View>
    </Pressable>
  );
}

Pattern: Shake Animation

function useShake() {
  const shakeAnim = useRef(new Animated.Value(0)).current;
  
  const shake = () => {
    Animated.sequence([
      Animated.timing(shakeAnim, { toValue: 10, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: -10, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: 10, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: -10, duration: 50, useNativeDriver: true }),
      Animated.timing(shakeAnim, { toValue: 0, duration: 50, useNativeDriver: true }),
    ]).start();
  };
  
  return { 
    shakeStyle: { transform: [{ translateX: shakeAnim }] }, 
    shake 
  };
}

// Usage
function ShakingInput() {
  const { shakeStyle, shake } = useShake();
  
  const handleError = () => {
    shake(); // Shake on validation error
  };
  
  return (
    <Animated.View style={shakeStyle}>
      <TextInput />
    </Animated.View>
  );
}

Pattern: Pulse Animation

function PulsingDot() {
  const scale = useRef(new Animated.Value(1)).current;
  const opacity = useRef(new Animated.Value(1)).current;
  
  useEffect(() => {
    const animation = Animated.loop(
      Animated.parallel([
        Animated.sequence([
          Animated.timing(scale, {
            toValue: 1.5,
            duration: 1000,
            useNativeDriver: true,
          }),
          Animated.timing(scale, {
            toValue: 1,
            duration: 1000,
            useNativeDriver: true,
          }),
        ]),
        Animated.sequence([
          Animated.timing(opacity, {
            toValue: 0.5,
            duration: 1000,
            useNativeDriver: true,
          }),
          Animated.timing(opacity, {
            toValue: 1,
            duration: 1000,
            useNativeDriver: true,
          }),
        ]),
      ])
    );
    
    animation.start();
    return () => animation.stop();
  }, []);
  
  return (
    <Animated.View
      style={[
        styles.dot,
        { transform: [{ scale }], opacity },
      ]}
    />
  );
}

Pattern: Expandable/Collapsible

function Expandable({ title, children }) {
  const [expanded, setExpanded] = useState(false);
  const animation = useRef(new Animated.Value(0)).current;
  
  const toggleExpand = () => {
    Animated.timing(animation, {
      toValue: expanded ? 0 : 1,
      duration: 300,
      useNativeDriver: false, // Height animation needs JS driver
    }).start();
    setExpanded(!expanded);
  };
  
  const heightInterpolate = animation.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 200], // Max height
  });
  
  const rotateInterpolate = animation.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '180deg'],
  });
  
  return (
    <View>
      <Pressable onPress={toggleExpand} style={styles.header}>
        <Text>{title}</Text>
        <Animated.Text style={{ transform: [{ rotate: rotateInterpolate }] }}>
          ▼
        </Animated.Text>
      </Pressable>
      
      <Animated.View style={{ height: heightInterpolate, overflow: 'hidden' }}>
        {children}
      </Animated.View>
    </View>
  );
}

Performance Tips

Animations can make or break your app's perceived performance. Here's how to ensure smooth 60fps animations.

1. Always Use Native Driver When Possible

// ✅ Good - runs on native thread
Animated.timing(opacity, {
  toValue: 1,
  duration: 300,
  useNativeDriver: true, // 60fps guaranteed
}).start();

// ⚠️ Less optimal - runs on JS thread
Animated.timing(width, {
  toValue: 200,
  duration: 300,
  useNativeDriver: false, // Can drop frames if JS is busy
}).start();

2. Use useRef for Animated Values

// ✅ Good - value persists across renders
const opacity = useRef(new Animated.Value(0)).current;

// ❌ Bad - creates new value every render
const opacity = new Animated.Value(0);

3. Avoid Animating Layout Properties

// ✅ Good - use transform for movement
style={{ transform: [{ translateX: animValue }] }}

// ⚠️ Avoid - triggers layout recalculation
style={{ marginLeft: animValue }}

// ✅ Good - use transform for size
style={{ transform: [{ scale: animValue }] }}

// ⚠️ Avoid - triggers layout recalculation  
style={{ width: animValue, height: animValue }}

4. Clean Up Animations

useEffect(() => {
  const animation = Animated.timing(opacity, {
    toValue: 1,
    duration: 1000,
    useNativeDriver: true,
  });
  
  animation.start();
  
  // Clean up on unmount
  return () => animation.stop();
}, []);

5. Avoid Many Simultaneous Animations

// ✅ Good - single value with interpolation
const progress = useRef(new Animated.Value(0)).current;

const opacity = progress.interpolate({ inputRange: [0, 1], outputRange: [0, 1] });
const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [50, 0] });
const scale = progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] });

// One animation drives all properties
Animated.timing(progress, { toValue: 1, useNativeDriver: true }).start();

// ⚠️ Less efficient - three separate animations
Animated.parallel([
  Animated.timing(opacity, { toValue: 1, useNativeDriver: true }),
  Animated.timing(translateY, { toValue: 0, useNativeDriver: true }),
  Animated.timing(scale, { toValue: 1, useNativeDriver: true }),
]).start();

🎯 Performance Checklist

  • ☐ Using useNativeDriver: true for opacity/transform
  • ☐ Animated values created with useRef
  • ☐ Using transform instead of layout properties
  • ☐ Single value with interpolation when possible
  • ☐ Animations cleaned up on unmount
  • ☐ Tested on low-end devices

The useAnimatedValue Hook

React Native 0.71+ provides useAnimatedValue, a cleaner way to create animated values. Let's also build our own custom hooks for reusable animation patterns.

Built-in useAnimatedValue

import { Animated, useAnimatedValue } from 'react-native';

function FadeComponent() {
  // Cleaner than useRef pattern
  const opacity = useAnimatedValue(0);
  
  useEffect(() => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();
  }, []);
  
  return (
    <Animated.View style={{ opacity }}>
      <Text>Fading in...</Text>
    </Animated.View>
  );
}

Custom Animation Hooks

Create reusable hooks for common animation patterns:

// hooks/useAnimation.ts
import { useRef, useCallback } from 'react';
import { Animated, Easing } from 'react-native';

export function useFadeAnimation(initialValue = 0) {
  const opacity = useRef(new Animated.Value(initialValue)).current;
  
  const fadeIn = useCallback((duration = 300) => {
    return new Promise<void>((resolve) => {
      Animated.timing(opacity, {
        toValue: 1,
        duration,
        useNativeDriver: true,
      }).start(() => resolve());
    });
  }, [opacity]);
  
  const fadeOut = useCallback((duration = 300) => {
    return new Promise<void>((resolve) => {
      Animated.timing(opacity, {
        toValue: 0,
        duration,
        useNativeDriver: true,
      }).start(() => resolve());
    });
  }, [opacity]);
  
  return { opacity, fadeIn, fadeOut };
}

// Usage
function FadeExample() {
  const { opacity, fadeIn, fadeOut } = useFadeAnimation(0);
  
  const handleToggle = async () => {
    await fadeOut();
    // Do something
    await fadeIn();
  };
  
  return (
    <Animated.View style={{ opacity }}>
      <Button title="Toggle" onPress={handleToggle} />
    </Animated.View>
  );
}

useSpringAnimation Hook

export function useSpringAnimation(initialValue = 0) {
  const value = useRef(new Animated.Value(initialValue)).current;
  
  const springTo = useCallback((toValue: number, config = {}) => {
    return new Promise<void>((resolve) => {
      Animated.spring(value, {
        toValue,
        useNativeDriver: true,
        ...config,
      }).start(() => resolve());
    });
  }, [value]);
  
  const reset = useCallback(() => {
    value.setValue(initialValue);
  }, [value, initialValue]);
  
  return { value, springTo, reset };
}

// Usage for press feedback
function SpringButton({ onPress, children }) {
  const { value, springTo } = useSpringAnimation(1);
  
  return (
    <Pressable
      onPressIn={() => springTo(0.95)}
      onPressOut={() => springTo(1)}
      onPress={onPress}
    >
      <Animated.View style={{ transform: [{ scale: value }] }}>
        {children}
      </Animated.View>
    </Pressable>
  );
}

useEntranceAnimation Hook

type EntranceType = 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'scale';

interface EntranceConfig {
  type?: EntranceType;
  duration?: number;
  delay?: number;
}

export function useEntranceAnimation({
  type = 'fade',
  duration = 400,
  delay = 0,
}: EntranceConfig = {}) {
  const animValue = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    Animated.timing(animValue, {
      toValue: 1,
      duration,
      delay,
      useNativeDriver: true,
    }).start();
  }, []);
  
  const getStyle = () => {
    const opacity = animValue;
    
    switch (type) {
      case 'fade':
        return { opacity };
        
      case 'slideUp':
        return {
          opacity,
          transform: [{
            translateY: animValue.interpolate({
              inputRange: [0, 1],
              outputRange: [30, 0],
            }),
          }],
        };
        
      case 'slideDown':
        return {
          opacity,
          transform: [{
            translateY: animValue.interpolate({
              inputRange: [0, 1],
              outputRange: [-30, 0],
            }),
          }],
        };
        
      case 'slideLeft':
        return {
          opacity,
          transform: [{
            translateX: animValue.interpolate({
              inputRange: [0, 1],
              outputRange: [30, 0],
            }),
          }],
        };
        
      case 'slideRight':
        return {
          opacity,
          transform: [{
            translateX: animValue.interpolate({
              inputRange: [0, 1],
              outputRange: [-30, 0],
            }),
          }],
        };
        
      case 'scale':
        return {
          opacity,
          transform: [{
            scale: animValue.interpolate({
              inputRange: [0, 1],
              outputRange: [0.8, 1],
            }),
          }],
        };
        
      default:
        return { opacity };
    }
  };
  
  return getStyle();
}

// Usage
function AnimatedCard() {
  const entranceStyle = useEntranceAnimation({ 
    type: 'slideUp', 
    delay: 200 
  });
  
  return (
    <Animated.View style={[styles.card, entranceStyle]}>
      <Text>I slide up!</Text>
    </Animated.View>
  );
}

Complete Animation Hook Library

// hooks/animations/index.ts
export { useFadeAnimation } from './useFadeAnimation';
export { useSpringAnimation } from './useSpringAnimation';
export { useEntranceAnimation } from './useEntranceAnimation';
export { useShakeAnimation } from './useShakeAnimation';
export { usePulseAnimation } from './usePulseAnimation';

// Example implementations for the others:

export function useShakeAnimation() {
  const translateX = useRef(new Animated.Value(0)).current;
  
  const shake = useCallback(() => {
    Animated.sequence([
      Animated.timing(translateX, { toValue: 10, duration: 50, useNativeDriver: true }),
      Animated.timing(translateX, { toValue: -10, duration: 50, useNativeDriver: true }),
      Animated.timing(translateX, { toValue: 10, duration: 50, useNativeDriver: true }),
      Animated.timing(translateX, { toValue: -10, duration: 50, useNativeDriver: true }),
      Animated.timing(translateX, { toValue: 0, duration: 50, useNativeDriver: true }),
    ]).start();
  }, [translateX]);
  
  return {
    style: { transform: [{ translateX }] },
    shake,
  };
}

export function usePulseAnimation(active = true) {
  const scale = useRef(new Animated.Value(1)).current;
  const animationRef = useRef<Animated.CompositeAnimation>();
  
  useEffect(() => {
    if (active) {
      animationRef.current = Animated.loop(
        Animated.sequence([
          Animated.timing(scale, {
            toValue: 1.1,
            duration: 500,
            useNativeDriver: true,
          }),
          Animated.timing(scale, {
            toValue: 1,
            duration: 500,
            useNativeDriver: true,
          }),
        ])
      );
      animationRef.current.start();
    } else {
      animationRef.current?.stop();
      scale.setValue(1);
    }
    
    return () => animationRef.current?.stop();
  }, [active, scale]);
  
  return { transform: [{ scale }] };
}

Hands-On Exercises

Exercise 1: Animated Button with Press Feedback

Create a button that scales down when pressed and springs back when released.

Show Solution
import { useRef } from 'react';
import { Animated, Pressable, Text, StyleSheet } from 'react-native';

function AnimatedButton({ title, onPress }) {
  const scale = useRef(new Animated.Value(1)).current;
  
  const handlePressIn = () => {
    Animated.spring(scale, {
      toValue: 0.92,
      useNativeDriver: true,
    }).start();
  };
  
  const handlePressOut = () => {
    Animated.spring(scale, {
      toValue: 1,
      friction: 3,
      useNativeDriver: true,
    }).start();
  };
  
  return (
    <Pressable
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      onPress={onPress}
    >
      <Animated.View 
        style={[
          styles.button,
          { transform: [{ scale }] }
        ]}
      >
        <Text style={styles.buttonText}>{title}</Text>
      </Animated.View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#2196F3',
    paddingVertical: 14,
    paddingHorizontal: 28,
    borderRadius: 8,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center',
  },
});

Exercise 2: Staggered List Animation

Create a list where items fade in and slide up one after another with a 100ms delay between each.

Show Solution
import { useRef, useEffect } from 'react';
import { Animated, View, Text, StyleSheet } from 'react-native';

interface Item {
  id: string;
  title: string;
}

function StaggeredList({ items }: { items: Item[] }) {
  const animatedValues = useRef(
    items.map(() => new Animated.Value(0))
  ).current;
  
  useEffect(() => {
    const animations = animatedValues.map((anim) =>
      Animated.timing(anim, {
        toValue: 1,
        duration: 400,
        useNativeDriver: true,
      })
    );
    
    Animated.stagger(100, animations).start();
  }, []);
  
  return (
    <View style={styles.list}>
      {items.map((item, index) => {
        const opacity = animatedValues[index];
        const translateY = animatedValues[index].interpolate({
          inputRange: [0, 1],
          outputRange: [30, 0],
        });
        
        return (
          <Animated.View
            key={item.id}
            style={[
              styles.item,
              {
                opacity,
                transform: [{ translateY }],
              },
            ]}
          >
            <Text style={styles.itemText}>{item.title}</Text>
          </Animated.View>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  list: {
    padding: 16,
  },
  item: {
    backgroundColor: '#fff',
    padding: 16,
    marginBottom: 12,
    borderRadius: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
  itemText: {
    fontSize: 16,
    color: '#333',
  },
});

Exercise 3: Loading Spinner

Create a continuously rotating loading spinner using Animated.loop.

Show Solution
import { useRef, useEffect } from 'react';
import { Animated, Easing, View, StyleSheet } from 'react-native';

function LoadingSpinner({ size = 40, color = '#2196F3' }) {
  const rotation = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    const animation = Animated.loop(
      Animated.timing(rotation, {
        toValue: 1,
        duration: 1000,
        easing: Easing.linear,
        useNativeDriver: true,
      })
    );
    
    animation.start();
    
    return () => animation.stop();
  }, [rotation]);
  
  const rotate = rotation.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  });
  
  return (
    <Animated.View
      style={[
        styles.spinner,
        {
          width: size,
          height: size,
          borderColor: color,
          borderTopColor: 'transparent',
          transform: [{ rotate }],
        },
      ]}
    />
  );
}

const styles = StyleSheet.create({
  spinner: {
    borderWidth: 3,
    borderRadius: 100,
  },
});

// Usage
function LoadingScreen() {
  return (
    <View style={styles.container}>
      <LoadingSpinner size={50} color="#6200EE" />
      <Text style={styles.text}>Loading...</Text>
    </View>
  );
}

Summary

You now have a solid foundation in React Native animations. These techniques cover the vast majority of animation needs in production apps.

🎯 Key Takeaways

  • Animated.Value stores numeric values that change over time
  • timing() for precise, timed animations with easing
  • spring() for natural, physics-based motion
  • interpolate() transforms one value into multiple outputs
  • parallel/sequence/stagger compose multiple animations
  • useNativeDriver: true is essential for 60fps performance
  • Custom hooks make animations reusable across components

Quick Reference

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

// Create value (in component)
const anim = useRef(new Animated.Value(0)).current;

// Timing animation
Animated.timing(anim, {
  toValue: 1,
  duration: 300,
  easing: Easing.ease,
  useNativeDriver: true,
}).start();

// Spring animation
Animated.spring(anim, {
  toValue: 1,
  friction: 7,
  tension: 40,
  useNativeDriver: true,
}).start();

// Interpolation
const opacity = anim.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 1],
});

// Composition
Animated.parallel([anim1, anim2]).start();
Animated.sequence([anim1, anim2]).start();
Animated.stagger(100, [anim1, anim2, anim3]).start();
Animated.loop(animation).start();

// Usage
<Animated.View style={{ opacity, transform: [{ scale: anim }] }}>

When to Use What

Scenario Recommended Approach
Fade in/out timing with 200-300ms
Button press spring with scale transform
Modal/sheet appear spring for natural feel
List items entering stagger + spring
Loading spinner loop + timing (linear)
Error shake sequence of quick timing

Coming Up Next

In the next lesson, we'll explore Theming and Dark Mode. You'll learn how to create a complete theming system and support light/dark mode throughout your app.