Animated and the Native Driver

React Native's built-in Animated API, what useNativeDriver can and cannot animate, and where LayoutAnimation fits.

SyntaxAnimated.timing(value, { toValue, duration, easing, useNativeDriver: true }).start(({ finished }) => {})

Animated is the animation API built into React Native. It exists because none of CSS is here: no transition property, no @keyframes, no animation shorthand. An animation is a JavaScript object you build, start and stop.

How it works

Keep a number in an Animated.Value, created once per component with useRef(new Animated.Value(0)).current. Pass it into the style of an Animated.View, Animated.Text, Animated.Image or Animated.ScrollView; a plain View ignores it. Animated.timing (duration defaults to 500ms), Animated.spring and Animated.decay change the number over time, and .start() runs the animation, optionally calling back with { finished }. interpolate maps one range onto another, so a single 0-to-1 value can drive opacity and offset at once. Animated.sequence, parallel, stagger, delay and loop combine animations.

The native driver

useNativeDriver: true hands the whole animation to the native side once, so no frame has to wait for JavaScript. It accepts non-layout properties only: transform and opacity, not width, height, top or left.

LayoutAnimation

LayoutAnimation is the blunt alternative: one call, and every view moved by the next layout pass animates into place. You cannot target a single view. LayoutAnimation.easeInEaseOut() is shorthand for LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) — the same call. On Android it does nothing until UIManager.setLayoutAnimationEnabledExperimental(true) has run.

The example below fades a card in and lifts it twelve points as the screen mounts. Busier screens and gesture work usually move to Reanimated, the next lesson.

Example

Example · tsx
import { useEffect, useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';

type NoticeProps = {
  title: string;
  body: string;
};

export default function Notice({ title, body }: NoticeProps) {
  // One value per component, created once. useRef keeps it across renders.
  const progress = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(progress, {
      toValue: 1,
      duration: 250,
      easing: Easing.out(Easing.quad), // decelerate into place
      useNativeDriver: true, // transform and opacity only
    }).start();
  }, [progress]);

  // One value drives two properties: interpolate maps 0..1 onto points.
  const translateY = progress.interpolate({
    inputRange: [0, 1],
    outputRange: [12, 0],
  });

  return (
    <View style={styles.screen}>
      <Animated.View
        style={[styles.card, { opacity: progress, transform: [{ translateY }] }]}>
        <Text style={styles.title}>{title}</Text>
        <Text>{body}</Text>
      </Animated.View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, justifyContent: 'center', padding: 16 },
  card: { backgroundColor: '#ffffff', borderRadius: 12, padding: 16, gap: 4 },
  title: { fontSize: 16, fontWeight: '600' },
});

When to use it

  • Fade and lift a card as a screen mounts, without adding a dependency.
  • Shrink a button while a finger is down using transform, so the native driver can run it.
  • Collapse a filter panel and let the content below close the gap in one call.
  • Drive a parallax header from scroll offset with Animated.event and the native driver.

More examples

Native driver or layout property - pick one

On screen: the blue bar widens from 80 to 240 points while the badge springs up by a fifth. The two animations look alike and are not: the badge runs natively and keeps its frame rate under load, the bar is recomputed in JavaScript each frame. Reach for this comparison when deciding whether a design can be expressed with transform and opacity, which is almost always worth the effort.

Example · tsx
import { useRef } from 'react';
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';

export default function BarAndBadge() {
  const scale = useRef(new Animated.Value(1)).current;
  const width = useRef(new Animated.Value(80)).current;

  const grow = () => {
    // transform: the native side can own this animation outright
    Animated.spring(scale, { toValue: 1.2, useNativeDriver: true }).start();
    // width is a layout property, so the native driver refuses it and
    // JavaScript has to compute every frame of this one
    Animated.timing(width, {
      toValue: 240,
      duration: 300,
      useNativeDriver: false,
    }).start();
  };

  return (
    <View style={styles.screen}>
      <Animated.View style={[styles.bar, { width }]} />
      <Animated.View style={[styles.badge, { transform: [{ scale }] }]}>
        <Text style={styles.badgeText}>3</Text>
      </Animated.View>
      <Pressable onPress={grow}>
        <Text>Grow both</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 },
  bar: { height: 8, borderRadius: 4, backgroundColor: '#1976d2' },
  badge: {
    width: 32,
    height: 32,
    borderRadius: 16,
    backgroundColor: '#c2185b',
    alignItems: 'center',
    justifyContent: 'center',
  },
  badgeText: { color: '#ffffff', fontWeight: '600' },
});

A pulse that loops until the screen unmounts

On screen: a red dot with a ring that swells outwards and fades, over and over, once every 1.2 seconds. Animated.loop with iterations defaulting to -1 runs forever, which is why the cleanup function calls loop.stop(); without it the animation keeps running after the screen is gone. Use this shape for recording indicators, live badges and anything that must keep signalling without user input.

Example · tsx
import { useEffect, useRef } from 'react';
import { Animated, Easing, StyleSheet, View } from 'react-native';

export default function RecordingDot() {
  const pulse = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const loop = Animated.loop(
      Animated.sequence([
        Animated.timing(pulse, {
          toValue: 1,
          duration: 600,
          easing: Easing.out(Easing.quad),
          useNativeDriver: true,
        }),
        Animated.timing(pulse, {
          toValue: 0,
          duration: 600,
          easing: Easing.in(Easing.quad),
          useNativeDriver: true,
        }),
      ]),
    );
    loop.start();
    return () => loop.stop(); // an endless animation must be stopped on unmount
  }, [pulse]);

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

  return (
    <View style={styles.screen}>
      <Animated.View style={[styles.ring, { opacity, transform: [{ scale }] }]} />
      <View style={styles.dot} />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  ring: { position: 'absolute', width: 24, height: 24, borderRadius: 12, backgroundColor: '#e53935' },
  dot: { width: 24, height: 24, borderRadius: 12, backgroundColor: '#e53935' },
});

LayoutAnimation, shorthand and longhand

On screen: the panel appears and the Results label slides down to make room, both eased over 300ms. Note what is animated: not the panel specifically, but every view the next layout pass moves. That is the whole trade - one line of code, no control over which views take part. Use it for a quick collapse or expand; use Reanimated's layout transitions when you need per-element control.

Example · tsx
import { useState } from 'react';
import {
  LayoutAnimation,
  Platform,
  Pressable,
  StyleSheet,
  Text,
  UIManager,
  View,
} from 'react-native';

// Android ignores LayoutAnimation until this has run once, at module load
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
  UIManager.setLayoutAnimationEnabledExperimental(true);
}

export default function FilterPanel() {
  const [open, setOpen] = useState(false);

  const toggle = () => {
    // Shorthand
    LayoutAnimation.easeInEaseOut();
    // Longhand - the identical call
    // LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    setOpen((wasOpen) => !wasOpen); // configure first, then change state
  };

  return (
    <View style={styles.screen}>
      <Pressable onPress={toggle}>
        <Text>{open ? 'Hide filters' : 'Show filters'}</Text>
      </Pressable>
      {open ? (
        <View style={styles.panel}>
          <Text>Price</Text>
          <Text>Rating</Text>
        </View>
      ) : null}
      <Text>Results</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16, gap: 12 },
  panel: { backgroundColor: '#eceff1', borderRadius: 8, padding: 12, gap: 8 },
});

Discussion

  • Be the first to comment on this lesson.