Tätä artikkelia ei ole vielä käännetty suomeksi. Näytetään englanninkielinen versio.
React Native Animations: From Basics to Advanced Techniques
Kirjoittanut React Native Finland
•Animations are what separate a good app from a great one. They provide feedback, guide attention, and make interactions feel natural. In React Native, you have several options for creating animations — from the built-in Animated API to the powerful React Native Reanimated library. This guide covers everything you need to know.
The Animation Landscape
React Native offers several animation approaches:
- Animated API — Built-in, good for simple animations
- LayoutAnimation — Automatic layout transitions
- React Native Reanimated — UI thread animations, best performance
- Moti — Declarative animations built on Reanimated
- Lottie — Complex vector animations from After Effects
For most apps, you'll use a combination of Reanimated for custom animations and Lottie for complex illustrative animations.
Animated API Basics
The built-in Animated API is a good starting point. It runs animations on the JavaScript thread, which works fine for simple use cases.
Creating an Animated Value
const opacity = new Animated.Value(0);
Running an Animation
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
Always set useNativeDriver: true when animating transform and opacity. This runs the animation on the UI thread for better performance.
Applying to Components
<Animated.View style={{ opacity }}>
<Text>I fade in!</Text>
</Animated.View>
A Complete Example
const FadeInView = ({ children }) => {
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
}, [fadeAnim]);
return (
<Animated.View style={{ opacity: fadeAnim }}>
{children}
</Animated.View>
);
};
React Native Reanimated
For serious animation work, React Native Reanimated is the standard. It runs entirely on the UI thread, giving you 60 FPS animations even when the JS thread is busy.
Installation
npx expo install react-native-reanimated
Add the plugin to your babel.config.js:
module.exports = {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
Shared Values
Reanimated uses "shared values" instead of Animated.Value:
const offset = useSharedValue(0);
Animated Styles
Create animated styles with useAnimatedStyle:
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const Box = () => {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
const handlePress = () => {
offset.value = withSpring(offset.value + 50);
};
return (
<Animated.View style={[styles.box, animatedStyle]}>
<Pressable onPress={handlePress}>
<Text>Tap me</Text>
</Pressable>
</Animated.View>
);
};
Animation Types
Reanimated provides several animation functions:
// Spring physics
offset.value = withSpring(100);
// Timing with easing
offset.value = withTiming(100, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
// Decay (momentum-based)
velocity.value = withDecay({
velocity: gestureVelocity,
clamp: [0, 200],
});
Sequencing and Combining
// Sequential animations
offset.value = withSequence(
withTiming(100),
withTiming(0),
);
// Delayed animation
offset.value = withDelay(500, withSpring(100));
// Repeating
offset.value = withRepeat(
withTiming(100),
-1, // infinite
true, // reverse
);
Gesture-Driven Animations
The real power of Reanimated comes with gesture handling. Combined with React Native Gesture Handler, you can create fluid, interactive animations.
Installation
npx expo install react-native-gesture-handler
Pan Gesture Example
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const DraggableBox = () => {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const context = useSharedValue({ x: 0, y: 0 });
const gesture = Gesture.Pan()
.onStart(() => {
context.value = {
x: translateX.value,
y: translateY.value,
};
})
.onUpdate((event) => {
translateX.value = context.value.x + event.translationX;
translateY.value = context.value.y + event.translationY;
})
.onEnd(() => {
// Snap back to origin
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.box, animatedStyle]} />
</GestureDetector>
);
};
Swipe to Delete
A common pattern in mobile apps:
const SwipeableRow = ({ onDelete, children }) => {
const translateX = useSharedValue(0);
const DELETE_THRESHOLD = -100;
const gesture = Gesture.Pan()
.activeOffsetX([-10, 10])
.onUpdate((event) => {
translateX.value = Math.min(0, event.translationX);
})
.onEnd(() => {
if (translateX.value < DELETE_THRESHOLD) {
translateX.value = withTiming(-500, {}, () => {
runOnJS(onDelete)();
});
} else {
translateX.value = withSpring(0);
}
});
const rowStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const deleteStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateX.value,
[DELETE_THRESHOLD, 0],
[1, 0],
),
}));
return (
<View>
<Animated.View style={[styles.deleteBackground, deleteStyle]}>
<Text style={styles.deleteText}>Delete</Text>
</Animated.View>
<GestureDetector gesture={gesture}>
<Animated.View style={rowStyle}>
{children}
</Animated.View>
</GestureDetector>
</View>
);
};
Interpolation
Interpolation maps input ranges to output ranges. Essential for complex animations:
const animatedStyle = useAnimatedStyle(() => {
const scale = interpolate(
progress.value,
[0, 0.5, 1],
[1, 1.2, 1],
Extrapolate.CLAMP,
);
const opacity = interpolate(
progress.value,
[0, 1],
[0.5, 1],
);
return {
transform: [{ scale }],
opacity,
};
});
Color Interpolation
const animatedStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(
progress.value,
[0, 1],
['#FF0000', '#00FF00'],
);
return { backgroundColor };
});
Layout Animations
Reanimated 2+ provides layout animations for entering, exiting, and layout changes:
const AnimatedList = ({ items }) => (
<View>
{items.map((item) => (
<Animated.View
key={item.id}
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
>
<ListItem item={item} />
</Animated.View>
))}
</View>
);
Built-in entering animations include:
FadeIn,FadeOutSlideInLeft,SlideInRight,SlideOutLeft,SlideOutRightZoomIn,ZoomOutBounceIn,BounceOut- And many more
Scroll Animations
Create scroll-driven animations with useAnimatedScrollHandler:
const scrollOffset = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollOffset.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => {
const height = interpolate(
scrollOffset.value,
[0, 100],
[200, 60],
Extrapolate.CLAMP,
);
return { height };
});
return (
<>
<Animated.View style={[styles.header, headerStyle]} />
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
{/* content */}
</Animated.ScrollView>
</>
);
Performance Tips
Do: Use Transform and Opacity
These properties can be animated on the UI thread:
// Good - UI thread
transform: [{ translateX }, { scale }, { rotate }]
opacity
// Bad - Layout triggers
width, height, margin, padding
Do: Avoid Worklet Overhead
Keep worklets simple. Heavy computations should happen on the JS thread:
// Bad - heavy computation in worklet
const animatedStyle = useAnimatedStyle(() => {
const result = heavyCalculation(values); // Don't do this
return { transform: [{ translateX: result }] };
});
// Good - compute on JS, animate the result
const computedValue = useMemo(() => heavyCalculation(values), [values]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: computedValue }],
}));
Do: Use Native Driver
With the Animated API, always use native driver for transform/opacity:
Animated.timing(value, {
toValue: 1,
duration: 300,
useNativeDriver: true, // Required for performance
}).start();
Lottie for Complex Animations
For complex illustrative animations, use Lottie:
npx expo install lottie-react-native
const Animation = () => (
<LottieView
source={require('./animation.json')}
autoPlay
loop
style={{ width: 200, height: 200 }}
/>
);
Designers can create animations in After Effects and export them as JSON. This is perfect for:
- Loading indicators
- Success/error states
- Onboarding illustrations
- Empty states
Moti: Declarative Animations
If you prefer a more declarative API, Moti builds on Reanimated:
npm install moti
const FadeIn = () => (
<MotiView
from={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'timing', duration: 500 }}
>
<Text>Hello!</Text>
</MotiView>
);
Moti is great for:
- Simple declarative animations
- Skeleton loading states
- Presence animations
Recap
- Start with Reanimated for most custom animations
- Use shared values and animated styles for UI thread performance
- Combine with Gesture Handler for interactive animations
- Stick to transform and opacity for best performance
- Use Lottie for complex illustrative animations
- Consider Moti for simple declarative animations
Animations are an investment that pays off in user experience. Start simple, profile performance, and gradually add polish to your app's most important interactions.