Introduction
Animations are the difference between an app that feels native and one that feels like a web page wrapped in a mobile shell. But creating smooth, 60fps animations in React Native has historically been challenging — the asynchronous bridge between JavaScript and native code introduced latency that made gesture-driven interactions and complex animations feel sluggish. React Native Reanimated solves this by moving animation logic to the native UI thread, executing JavaScript-like code through "worklets" that run synchronously on the UI thread with zero bridge crossings.
Reanimated has become the de facto standard animation library for React Native, used in production by apps with hundreds of millions of users. Its API is designed around shared values that animate on the native thread, gesture-driven animations that respond in real-time, and layout animations that handle mount/unmount transitions gracefully. Combined with React Native Gesture Handler, Reanimated enables the creation of interactions that are indistinguishable from native iOS and Android applications.
Understanding Reanimated: Core Concepts
The Problem with Animated API
React Native's built-in Animated API has a critical limitation: it uses the bridge to communicate animation updates. Each frame, the JavaScript thread calculates the next animation value, serializes it, sends it across the bridge, and the native thread applies it. At 60fps, this means 60 round trips per second through the asynchronous bridge.
When the JavaScript thread is busy (during navigation transitions, list rendering, or data processing), these bridge messages queue up, causing visible animation stutter. The useNativeDriver flag helps for simple animations but doesn't support layout properties like width, height, or flex.
Reanimated's Architecture: Worklets and Shared Values
Reanimated introduces two fundamental concepts that eliminate bridge dependency:
Worklets: Functions marked with 'worklet' directive that are compiled and executed on the UI thread. They look and feel like regular JavaScript but run natively, bypassing the bridge entirely.
'use worklet';
// This function runs on the UI thread — zero bridge crossings
const animatedStyle = (progress) => {
'worklet';
return {
opacity: progress.value,
transform: [{ translateY: progress.value * 50 }],
};
};Shared Values: Mutable values that can be read and written from both the JavaScript thread and the UI thread without synchronization overhead. They're the bridge between your React state and your native animations.
const opacity = useSharedValue(0);
// Write from JS thread (e.g., in response to API data)
opacity.value = 1;
// Read on UI thread (in animated styles)
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));Animation Hooks
Reanimated provides a comprehensive set of animation primitives:
useSharedValue()— Creates a mutable shared valueuseAnimatedStyle()— Derives animated styles from shared valuesuseDerivedValue()— Creates computed values that update automaticallyuseAnimatedGestureHandler()— Handles gestures on the UI thread (deprecated in v3 in favor of Gesture Handler integration)withTiming()— Timing-based animations with easing curveswithSpring()— Physics-based spring animationswithDecay()— Momentum-based deceleration animationswithSequence()— Chains multiple animationswithRepeat()— Loops animations
Architecture and Design Patterns
Shared Value Lifecycle
Shared values are the backbone of Reanimated's architecture. They use a dual-copy mechanism: one copy lives on the JavaScript thread and one on the UI thread. Updates are synchronized lazily — when the UI thread needs the latest value, it fetches it, minimizing synchronization overhead.
function AnimatedCard() {
// Shared value: accessible from both JS and UI threads
const translateX = useSharedValue(0);
const scale = useSharedValue(1);
// Derived value: computed from shared values on UI thread
const borderRadius = useDerivedValue(() => {
return interpolate(scale.value, [1, 1.2], [12, 24], Extrapolation.CLAMP);
});
// Animated style: applied to component on UI thread
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ scale: scale.value },
],
borderRadius: borderRadius.value,
}));
return (
<Animated.View style={[styles.card, animatedStyle]}>
<Text>Swipe me</Text>
</Animated.View>
);
}Animation Composition
Reanimated's animation functions can be composed to create complex behaviors:
const position = useSharedValue({ x: 0, y: 0 });
function animateToPosition(targetX: number, targetY: number) {
// Run animations in parallel on the UI thread
position.value = {
x: withSpring(targetX, { damping: 15, stiffness: 150 }),
y: withTiming(targetY, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) }),
};
}
function animateSequence() {
translateX.value = withSequence(
withTiming(100, { duration: 200 }),
withTiming(-50, { duration: 150 }),
withSpring(0, { damping: 8 })
);
}Layout Animations
Reanimated v2+ provides declarative layout animations for mount/unmount transitions:
import Animated, {
FadeInDown,
FadeOutLeft,
Layout,
SlideInRight,
} from 'react-native-reanimated';
function TodoList({ items }: { items: Todo[] }) {
return (
<FlatList
data={items}
renderItem={({ item, index }) => (
<Animated.View
entering={FadeInDown.delay(index * 100).springify()}
exiting={FadeOutLeft.duration(200)}
layout={Layout.springify()}
style={styles.todoItem}
>
<Text>{item.title}</Text>
</Animated.View>
)}
/>
);
}Step-by-Step Implementation
Installation
# Install Reanimated
npx expo install react-native-reanimated
# Install Gesture Handler (recommended companion)
npx expo install react-native-gesture-handlerBabel Configuration
Add the Reanimated Babel plugin (required for worklet compilation):
// babel.config.js
module.exports = {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'], // Must be last
};Your First Animated Component
import { StyleSheet, Pressable, Text } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
export function AnimatedButton() {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const handlePressIn = () => {
scale.value = withTiming(0.95, { duration: 100 });
opacity.value = withTiming(0.8, { duration: 100 });
};
const handlePressOut = () => {
scale.value = withSpring(1, { damping: 10, stiffness: 200 });
opacity.value = withTiming(1, { duration: 150 });
};
return (
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
<Animated.View style={[styles.button, animatedStyle]}>
<Text style={styles.buttonText}>Press Me</Text>
</Animated.View>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#3b82f6',
paddingHorizontal: 24,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});Implementing a Swipe-to-Dismiss Gesture
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from 'react-native-reanimated';
export function SwipeToDismiss({
children,
onDismiss,
}: {
children: React.ReactNode;
onDismiss: () => void;
}) {
const translateX = useSharedValue(0);
const opacity = useSharedValue(1);
const gesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
opacity.value = 1 - Math.abs(event.translationX) / 300;
})
.onEnd((event) => {
if (Math.abs(event.translationX) > 150) {
translateX.value = withSpring(event.translationX > 0 ? 400 : -400);
opacity.value = withTiming(0, { duration: 200 });
runOnJS(onDismiss)();
} else {
translateX.value = withSpring(0);
opacity.value = withTiming(1, { duration: 200 });
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
opacity: opacity.value,
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={animatedStyle}>{children}</Animated.View>
</GestureDetector>
);
}Real-World Use Cases
Use Case 1: Scroll-Linked Parallax Header
import Animated, {
useAnimatedScrollHandler,
useAnimatedStyle,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
export function ParallaxScrollView({ children }: { children: React.ReactNode }) {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(
scrollY.value,
[-100, 0],
[300, 200],
Extrapolation.CLAMP
),
opacity: interpolate(
scrollY.value,
[0, 150],
[1, 0],
Extrapolation.CLAMP
),
}));
const titleStyle = useAnimatedStyle(() => ({
transform: [{
translateY: interpolate(
scrollY.value,
[0, 200],
[0, -80],
Extrapolation.CLAMP
),
}],
fontSize: interpolate(
scrollY.value,
[0, 200],
[28, 18],
Extrapolation.CLAMP
),
}));
return (
<View style={styles.container}>
<Animated.View style={[styles.header, headerStyle]}>
<Animated.Text style={[styles.title, titleStyle]}>
Parallax Header
</Animated.Text>
</Animated.View>
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
{children}
</Animated.ScrollView>
</View>
);
}Use Case 2: Animated Bottom Sheet
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const SHEET_HEIGHT = 400;
const SNAP_POINTS = { closed: SHEET_HEIGHT, half: SHEET_HEIGHT / 2, open: 0 };
export function BottomSheet({ children }: { children: React.ReactNode }) {
const translateY = useSharedValue(SHEET_HEIGHT);
const context = useSharedValue(0);
const gesture = Gesture.Pan()
.onStart(() => {
context.value = translateY.value;
})
.onUpdate((event) => {
translateY.value = Math.max(
SNAP_POINTS.open,
context.value + event.translationY
);
})
.onEnd((event) => {
const velocity = event.velocityY;
const position = translateY.value;
if (velocity < -500) {
translateY.value = withSpring(SNAP_POINTS.open, { damping: 20 });
} else if (velocity > 500) {
translateY.value = withSpring(SNAP_POINTS.closed, { damping: 20 });
} else if (position < SHEET_HEIGHT / 3) {
translateY.value = withSpring(SNAP_POINTS.open, { damping: 20 });
} else {
translateY.value = withSpring(SNAP_POINTS.closed, { damping: 20 });
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
const backdropStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateY.value,
[0, SHEET_HEIGHT],
[0.5, 0],
Extrapolation.CLAMP
),
}));
return (
<>
<Animated.View style={[styles.backdrop, backdropStyle]} />
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.sheet, animatedStyle]}>
<View style={styles.handle} />
{children}
</Animated.View>
</GestureDetector>
</>
);
}Use Case 3: Skeleton Loading Animation
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
withSequence,
Easing,
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
export function SkeletonLoader({ width, height }: { width: number; height: number }) {
const translateX = useSharedValue(-width);
useEffect(() => {
translateX.value = withRepeat(
withSequence(
withTiming(-width, { duration: 0 }),
withTiming(width, { duration: 1000, easing: Easing.linear })
),
-1, // Infinite repeat
false
);
}, []);
const shimmerStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<View style={[styles.skeleton, { width, height }]}>
<Animated.View style={[styles.shimmer, shimmerStyle]}>
<LinearGradient
colors={['transparent', 'rgba(255,255,255,0.3)', 'transparent']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={StyleSheet.absoluteFill}
/>
</Animated.View>
</View>
);
}Best Practices for Production
-
Always run expensive calculations in worklets: If your animation interpolation involves multiple values, compute it in a worklet rather than on the JS thread to prevent bridge crossings.
-
Use
withSpringfor natural-feeling interactions: Spring physics produce animations that feel physical and responsive. Adjustdamping(lower = bouncier) andstiffness(higher = faster) to match your design language. -
Minimize shared value updates: Each shared value update triggers UI thread work. Batch related updates using
runOnJSsparingly and prefer derived values for computed properties. -
Use layout animations for list items:
FadeIn,SlideInRight, andLayoutanimations make FlatList items feel polished without manual animation setup. -
Prevent memory leaks with cleanup: Cancel ongoing animations in
useEffectcleanup by setting shared values to their final state. -
Profile with Reanimated's dedicated tools: Use the Reanimated plugin's console warnings to detect worklets that accidentally reference JS-only variables.
-
Keep worklets pure: Worklets should not access closures over React component state. Only use shared values and constants passed as arguments.
-
Test on physical devices: Emulators may mask performance issues. Always test animations on real devices, especially low-end Android phones.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Babel plugin not last in config | Worklets fail to compile silently | Ensure react-native-reanimated/plugin is the last entry in plugins array |
| Accessing component state inside worklets | Runtime error or stale values | Pass values through shared values, not React state references |
Forgetting scrollEventThrottle on scroll handlers | Scroll-linked animations update at low framerate | Set scrollEventThrottle={16} for 60fps scroll events |
| Spring animation never settling | Animation continues consuming CPU | Increase damping or set restDisplacementThreshold and restSpeedThreshold |
| Layout animations causing FlatList performance issues | List scrolling becomes janky | Use itemLayoutAnimation prop on FlatList instead of wrapping individual items |
| Shared value read on wrong thread | Undefined behavior or crashes | Ensure worklets use value property, not the shared value object itself |
Performance Optimization
Measuring Animation Performance
import { configureReanimatedLogger, LogLevel } from 'react-native-reanimated';
// Enable strict mode to detect performance issues
configureReanimatedLogger({
level: LogLevel.warn,
strict: true, // Warns when worklets access JS-only values
});Reducing Re-renders
// Bad: Component re-renders on every frame
function BadAnimation() {
const [offset, setOffset] = useState(0);
useEffect(() => {
const interval = setInterval(() => setOffset(prev => prev + 1), 16);
return () => clearInterval(interval);
}, []);
return <View style={{ transform: [{ translateY: offset }] }} />;
}
// Good: Animation runs entirely on UI thread
function GoodAnimation() {
const translateY = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
useEffect(() => {
translateY.value = withRepeat(withTiming(50, { duration: 1000 }), -1, true);
}, []);
return <Animated.View style={style} />;
}Comparison with Alternatives
| Feature | Reanimated | Animated API | Lottie | Moti |
|---|---|---|---|---|
| UI Thread Execution | Yes (worklets) | With useNativeDriver | Yes (pre-rendered) | Built on Reanimated |
| Gesture Integration | Deep (Gesture Handler) | Limited | None | Built on Reanimated |
| Layout Animations | Built-in | Not available | Not available | Declarative API |
| Spring Physics | Native springs | Basic springs | Not available | Declarative springs |
| Learning Curve | Medium-High | Low | Low | Low-Medium |
| Bundle Size | ~80KB | Built-in | ~150KB | ~20KB (wraps Reanimated) |
| Complexity Support | Very high | Low-Medium | Pre-defined only | Medium |
Testing Strategies
import { render } from '@testing-library/react-native';
import Animated from 'react-native-reanimated';
// Mock Reanimated for tests
jest.mock('react-native-reanimated', () =>
require('react-native-reanimated/mock')
);
describe('AnimatedButton', () => {
it('should render without crashing', () => {
const { getByText } = render(<AnimatedButton />);
expect(getByText('Press Me')).toBeTruthy();
});
it('should handle press gesture', async () => {
const onPress = jest.fn();
const { getByText } = render(<AnimatedButton onPress={onPress} />);
fireEvent.press(getByText('Press Me'));
expect(onPress).toHaveBeenCalled();
});
});Shared Element Transitions and Layout Animations
Shared element transitions create seamless visual connections between screens by animating elements that appear in both the source and destination screens. Reanimated v3 introduced the SharedTransition API that automatically captures the position and size of tagged elements on both screens and interpolates between them during navigation transitions. Tag elements with sharedTransitionTag props on both screens, and the navigation library handles the animation orchestration automatically.
Layout animations in Reanimated apply automatic enter and exit animations when components mount or unmount from the component tree. The Layout animation modifier specifies how elements animate when their position or size changes due to layout recalculations. Combining entering, exiting, and Layout modifiers on a single component creates fluid animations that respond to data changes without manual animation configuration.
The LinearTransition layout animation interpolates linearly between the old and new layout positions, while SpringTransition uses a spring physics model for more natural motion. For list reordering, combining layout animations with FlatList or FlashList creates smooth reorder effects where items slide into their new positions. The animation duration and spring parameters are configurable to match your design system's motion specifications.
Debugging Reanimated Animations
Debugging worklet-based animations requires specialized tools because worklets execute on a separate UI thread that standard debugging tools cannot inspect. The Reanimated plugin injects console logging into worklets, so console.log calls inside useAnimatedStyle callbacks appear in the Metro console. However, stepping through worklet code with a debugger is not supported, so logging and visual debugging are the primary techniques.
The Reanimated DevTools Chrome extension provides a visual timeline of all running animations, showing the progress, velocity, and current value of each animated property. You can pause, rewind, and scrub through animations frame by frame to identify unexpected jumps or timing issues. The devtools also display the worklet execution time, helping identify performance bottlenecks in complex animation compositions.
For layout animation debugging, enable the layoutAnimationRunner log mode to see the measured positions and sizes of elements before and after layout changes. Common issues include elements measuring incorrectly due to flex layout quirks, animations running in the wrong direction due to coordinate system assumptions, and enter animations firing before the element has received its final dimensions from the layout system.
Future Outlook
Reanimated v3 brought the New Architecture support and improved worklet compilation. The roadmap includes shared element transitions (shared layout animations between screens), web animation support for React Native Web, and deeper integration with the Fabric renderer for even lower-latency gesture responses. The convergence of Reanimated and Gesture Handler into a unified animation system will simplify the API while maintaining the performance characteristics that make it the gold standard for React Native animations.
Reanimated vs LayoutAnimation
React Native's built-in LayoutAnimation API provides simple layout animations but has significant limitations compared to Reanimated. LayoutAnimation runs on the JS thread and can cause dropped frames during complex animations. It also animates all layout changes globally, making it difficult to animate specific elements independently. Reanimated runs animations on the UI thread using native drivers, ensuring smooth 60fps performance even during JS thread blocking operations like state updates and data processing.
Conclusion
React Native Reanimated transforms mobile animations from a performance liability into a competitive advantage. By executing animation logic on the native UI thread through worklets, it eliminates the bridge bottleneck that has historically limited React Native's animation capabilities.
Key takeaways:
- Worklets run on the UI thread — zero bridge crossings for animation logic
- Shared values are the bridge between JS and UI threads — mutable, synchronized, fast
- Spring physics produce natural-feeling interactions — use
withSpringas the default - Layout animations are declarative —
FadeIn,SlideInRight,Layoutfor mount/unmount - Gesture Handler integration is seamless — animate in response to gestures without performance cost
- Scroll-linked animations are frame-perfect —
useAnimatedScrollHandlerruns on UI thread - Profile with strict mode — catch JS-thread access in worklets during development
Install Reanimated today with npx expo install react-native-reanimated react-native-gesture-handler and build animations that feel truly native.