Animations
A screen that just snaps between states feels cheap; a screen that eases, fades, and slides feels
polished. React Native ships with a built-in animation system called Animated that
handles most of what an app actually needs — fades, slides, scales, rotations — without pulling in
any extra library. This page covers Animated in depth, then gives you an honest,
conceptual look at the two tools you'll reach for once your animation needs get more advanced:
LayoutAnimation and react-native-reanimated.
Animated.Value: A Value React Can Animate Smoothly
Ordinary React state (from useState, covered in
State & Hooks) triggers a full re-render every time
it changes — great for toggling things on and off, but far too slow to drive 60 frames per second
of smooth motion. Animated.Value is a special container built specifically for
animation: React Native can update it many times a second, often without re-rendering your
component at all.
You almost always create an Animated.Value with useRef rather than
useState. The reason is important: useRef gives you a plain object that
persists across re-renders without ever causing one itself. If you created the
Animated.Value with plain useState instead, a fresh one would be created
on every re-render — losing whatever animation progress it had — which defeats the point entirely.
import React, { useRef, useEffect } from 'react';
import { Animated, Text } from 'react-native';
export default function FadeInText() {
// useRef keeps this SAME Animated.Value alive across every re-render
const fadeAnim = useRef(new Animated.Value(0)).current;
// ... animation logic goes here, see next section
return (
<Animated.Text style={{ opacity: fadeAnim }}>
Welcome!
</Animated.Text>
);
}
Note the component name: Animated.Text, not plain Text. Only components
wrapped by Animated (Animated.View, Animated.Text,
Animated.Image, or your own component wrapped with Animated.createAnimatedComponent)
know how to accept an Animated.Value directly in their style.
Animating a Value with Animated.timing()
Animated.timing() moves an Animated.Value from its current value to a
target toValue over a given duration, using an easing curve. Calling
.start() actually kicks the animation off. A common pattern is starting it once, on
mount, using useEffect with an empty dependency array.
import React, { useRef, useEffect } from 'react';
import { Animated, Text } from 'react-native';
export default function FadeInText() {
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
}, []); // empty deps: run once, when the component first mounts
return (
<Animated.Text style={{ opacity: fadeAnim }}>
Welcome!
</Animated.Text>
);
}
fadeAnim starts at 0 (fully transparent) and, over 500 milliseconds, eases
up to 1 (fully opaque). Because fadeAnim is wired directly into
opacity, the text visibly fades in on screen — no manual frame-by-frame updates needed.
The Critical useNativeDriver Option
useNativeDriver: true is one of the most important options in the whole
Animated API, and it's worth understanding exactly what it does. By default,
JavaScript computes every frame of an animation and sends each new value across to the native side
— meaning if your JS thread ever gets busy (a heavy render, a big list update, a network callback),
the animation can visibly stutter. Setting useNativeDriver: true instead hands the
entire animation off to the native side up front, so it plays smoothly on its own thread
independent of whatever JavaScript is doing at that moment.
opacity and transform (translate, scale, rotate). It does not support backgroundColor, or layout-affecting properties like width, height, top, or margin — those still animate on the JS thread and need useNativeDriver: false (or the default). Always animate opacity/transform when you can; reach for a layout property only when you must.Interpolating a Value with .interpolate()
A single Animated.Value often needs to drive something other than a plain 0-to-1
number — a rotation in degrees, a color, a position in pixels. .interpolate() maps an
input range onto a different output range, letting one underlying animated value power a
completely different-looking effect. A classic example is a full 360-degree spin, driven by the
same 0-to-1 value used for the fade above.
import React, { useRef, useEffect } from 'react';
import { Animated, Easing } from 'react-native';
export default function SpinningLoader() {
const spinAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(spinAnim, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}).start();
}, []);
// Map the 0..1 progress value onto a 0deg..360deg rotation
const rotate = spinAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
return (
<Animated.View style={{ transform: [{ rotate }] }}>
<Text>⟳</Text>
</Animated.View>
);
}
inputRange and outputRange must have the same number of entries and both
must be in ascending (or both descending) order — interpolate() works for more than
just two-point ranges too, letting you map several checkpoints along the animation to different
output values.
LayoutAnimation: Animating Layout Changes Automatically
Animated is great when you explicitly drive a value yourself. But sometimes what
changes is just your component tree's layout — an item gets added to a list, or removed, and every
sibling needs to smoothly slide into its new position rather than snapping there abruptly.
LayoutAnimation handles exactly that case with far less code: you call
LayoutAnimation.configureNext(...) right before the state update that changes your
layout (like removing an item from an array), and React Native automatically animates every
resulting layout change for you — no manual Animated.Value per item required.
react-native-reanimated: The Modern, More Powerful Option
Animated covers most everyday needs, but complex, gesture-driven animations — dragging
a card and having it follow your finger in real time, chained physics-based motion, animations that
must respond instantly to touch without any JS-thread round trip at all — tend to outgrow it.
react-native-reanimated is the community's de facto standard library for this tier of
animation. It runs animation and gesture logic almost entirely on the native/UI thread using
"worklets," giving noticeably smoother results for advanced interactions than the built-in API can
comfortably achieve. It's commonly paired with react-native-gesture-handler for
swipe/drag/pinch gestures. Treat this section as awareness rather than a how-to — reach for
react-native-reanimated once you find yourself fighting the built-in
Animated API for a genuinely complex, interactive animation.