Animated and the Native Driver
React Native's built-in Animated API, what useNativeDriver can and cannot animate, and where LayoutAnimation fits.
Animated.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
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.
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.
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.
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