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
opacitytransform(translateX/Y, scale, rotate)
Run on UI thread, best performance
⚠️ JS Thread Only
width,heightbackgroundColormargin,paddingborderRadius
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
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();
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>
);
}
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: truefor 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.