React Native Animations

Animations make your app feel alive and professional. A loading spinner, a sliding menu, a button that bounces when tapped — these small movements guide attention, communicate state changes, and create delight. React Native provides two animation systems: the built-in Animated API and the more powerful react-native-reanimated library.

The Animated API — Built-In

The Animated API works by creating special animated values that track a number over time. React Native reads these values to update styles without re-rendering the JavaScript component.

Animated Value       Drives         Visual Change
─────────────────────────────────────────────────────────
opacityAnim: 0→1     opacity        fade in
slideAnim: -100→0    translateX     slide from left
scaleAnim: 0.5→1     scale          grow from small
rotateAnim: 0→360    rotate         spin

Fade In Animation

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

export default function FadeInCard() {
  const opacity = useRef(new Animated.Value(0)).current;
  // starts at 0 (invisible)

  useEffect(() => {
    Animated.timing(opacity, {
      toValue: 1,          // animate to fully visible
      duration: 800,       // takes 800 milliseconds
      useNativeDriver: true,  // runs on native thread — smoother
    }).start();
  }, []);

  return (
    <Animated.View style={{ opacity, padding: 20, backgroundColor: '#007AFF', borderRadius: 12 }}>
      <Text style={{ color: 'white' }}>I faded in!</Text>
    </Animated.View>
  );
}
Timeline:
t=0ms   opacity=0.0  →  invisible
t=200ms opacity=0.25
t=400ms opacity=0.50
t=600ms opacity=0.75
t=800ms opacity=1.0  →  fully visible

Three Types of Animated Animations

timing — Smooth, Linear or Eased

Animated.timing(value, {
  toValue: 1,
  duration: 500,
  easing: Easing.ease,    // accelerate then decelerate
  useNativeDriver: true,
}).start();

spring — Bouncy Physics

Animated.spring(value, {
  toValue: 1,
  damping: 10,      // how quickly bouncing settles
  stiffness: 100,   // how stiff the spring is
  useNativeDriver: true,
}).start();

decay — Momentum and Slowdown

Animated.decay(value, {
  velocity: 0.5,    // starting speed
  deceleration: 0.997,
  useNativeDriver: true,
}).start();

Slide-In from Bottom

export default function SlideUpPanel() {
  const translateY = useRef(new Animated.Value(300)).current;
  // 300 = starts 300 points below visible area

  useEffect(() => {
    Animated.spring(translateY, {
      toValue: 0,         // slide to its natural position
      damping: 15,
      stiffness: 120,
      useNativeDriver: true,
    }).start();
  }, []);

  return (
    <Animated.View style={{
      transform: [{ translateY }],
      position: 'absolute', bottom: 0, left: 0, right: 0,
      backgroundColor: 'white', padding: 20, borderTopLeftRadius: 20, borderTopRightRadius: 20,
    }}>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Slide-Up Panel</Text>
    </Animated.View>
  );
}
Before animation:       After animation:
┌──────────────────┐    ┌──────────────────┐
│                  │    │                  │
│                  │    │                  │
│                  │    ├──────────────────┤
│                  │    │ Slide-Up Panel   │
│                  │    │                  │
└──────────────────┘    └──────────────────┘
  panel below screen      panel visible at bottom

Sequence and Parallel Animations

Run multiple animations one after another with Animated.sequence, or at the same time with Animated.parallel.

const opacity   = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(20)).current;

// Parallel: fade in AND slide up at the same time
Animated.parallel([
  Animated.timing(opacity,    { toValue: 1, duration: 400, useNativeDriver: true }),
  Animated.timing(translateY, { toValue: 0, duration: 400, useNativeDriver: true }),
]).start();

// Sequence: first fade in, THEN slide
Animated.sequence([
  Animated.timing(opacity,    { toValue: 1, duration: 300, useNativeDriver: true }),
  Animated.timing(translateY, { toValue: 0, duration: 300, useNativeDriver: true }),
]).start();

Looping Animation — Pulse Effect

const scale = useRef(new Animated.Value(1)).current;

useEffect(() => {
  Animated.loop(
    Animated.sequence([
      Animated.timing(scale, { toValue: 1.1, duration: 500, useNativeDriver: true }),
      Animated.timing(scale, { toValue: 1.0, duration: 500, useNativeDriver: true }),
    ])
  ).start();
}, []);

<Animated.View style={{ transform: [{ scale }], ... }}>
  <Text>🔴 Live</Text>
</Animated.View>

Interpolation — Map One Range to Another

Interpolation lets one animated value drive multiple style properties with different ranges.

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

// As user scrolls from 0 to 100, header shrinks from 80 to 50
const headerHeight = scrollY.interpolate({
  inputRange:  [0,  100],
  outputRange: [80, 50],
  extrapolate: 'clamp',  // don't go below 50 or above 80
});

// As user scrolls from 0 to 100, header text fades out
const headerOpacity = scrollY.interpolate({
  inputRange:  [0, 80],
  outputRange: [1,  0],
  extrapolate: 'clamp',
});
Scroll: 0                  Scroll: 100
┌─────────────────────┐    ┌─────────────────────┐
│  Large Header       │    │ Small Header        │
│  (80px, visible)    │    │ (50px, faded)       │
├─────────────────────┤    ├─────────────────────┤
│  Content            │    │  Content            │
└─────────────────────┘    └─────────────────────┘

react-native-reanimated — High-Performance Animations

react-native-reanimated runs entirely on the native thread. It handles gesture-driven animations without any JavaScript delay between the finger and the animation.

npx expo install react-native-reanimated

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

export default function BouncingBox() {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  function onPress() {
    scale.value = withSpring(1.2, { damping: 4 }, () => {
      scale.value = withSpring(1);   // bounce back
    });
  }

  return (
    <Pressable onPress={onPress}>
      <Animated.View style={[{ width: 80, height: 80, backgroundColor: '#007AFF', borderRadius: 12 }, animatedStyle]} />
    </Pressable>
  );
}

When to Use Which System

Animated API (built-in)         react-native-reanimated
──────────────────────────────────────────────────────────────
Simple fade, slide, scale       Complex gesture-driven animations
One-shot animations             Swipe cards, drag handles, stories
No extra install needed         Best performance for heavy animations
Good for beginners              Production apps with rich UI

Summary

Use Animated.Value with Animated.timing or Animated.spring for straightforward animations. Always set useNativeDriver: true for smooth performance. Use Animated.parallel and Animated.sequence to compose multiple animations. Use interpolate to link one animated value to multiple style changes. Choose react-native-reanimated for gesture-driven or very complex animation scenarios.

Leave a Comment