Deep Dive React Native Reanimated

Build buttery-smooth 60 FPS mobile animations with React Native Reanimated. Master CSS animations, worklets, and gesture-driven interactions.

Why React Native Reanimated Matters

Modern mobile users have zero tolerance for janky animations or interfaces that freeze during scroll. Research consistently shows that animation quality directly impacts user perception of app quality and brand credibility. React Native Reanimated addresses the fundamental limitation of JavaScript-thread animations by moving animation execution to the native UI thread, eliminating the communication overhead that causes frame drops in traditional approaches.

The library has evolved significantly since its initial release, with Reanimated 4 introducing CSS-like declarative animations alongside the powerful worklet-based API. This evolution gives developers flexibility to choose the right tool for each animation scenario.

For teams building React Native applications, mastering Reanimated is essential for creating the polished, responsive interfaces that users expect from modern mobile apps.

Two Animation Approaches

Reanimated 4 offers two distinct animation systems for different use cases

CSS Animations

Declarative animations for state-driven transitions. Simply define which properties to animate and let Reanimated handle the interpolation between values.

Worklets

Imperative, frame-by-frame control for gesture-driven interactions and scroll-linked effects. Run directly on the UI thread for maximum performance.

Shared Values

Reactive primitives that exist on both JavaScript and UI threads, enabling seamless communication without bridge overhead.

Gesture Integration

Seamless integration with react-native-gesture-handler for drag, pan, pinch, and other touch interactions.

Core Concepts: Worklets and Shared Values

What Are Worklets?

Worklets are small pieces of JavaScript code that run directly on the UI thread. Unlike regular JavaScript functions that execute on the JavaScript thread, worklets execute in the native layer where animations run. This direct execution eliminates the bridge communication overhead that causes performance issues in traditional React Native animations.

In Reanimated 4, worklet functionality has been extracted into a separate package called react-native-worklets-core, allowing other libraries to leverage the same infrastructure. To create a worklet, you add the 'worklet' directive at the beginning of a function:

function animateScale() {
 'worklet';
 scale.value = withSpring(1.2);
}

The 'worklet' directive is the magic that enables this cross-thread execution. Without it, the function would attempt to run on the JavaScript thread and fail to access shared values properly.

Shared Values: The Bridge Between Threads

Shared values are special reactive primitives that exist simultaneously in both the JavaScript and UI threads. They enable seamless communication between your React components and worklet animations without the performance penalty of bridge crossings. When you update a shared value from JavaScript, the UI thread sees the change immediately.

const scale = useSharedValue(1);

scale.value = 0.9; // Updates the shared value
console.log(scale.value); // Reads the current value

The UI Thread Advantage

Running animations on the UI thread provides consistent frame rates regardless of JavaScript thread load. When your React component is re-rendering due to state changes, animations on the UI thread continue uninterrupted. This separation of concerns means user interactions always feel responsive, even during computationally intensive operations.

Essential Hooks for Animation

useSharedValue: Creating Animated State

The useSharedValue hook creates a shared value that can be accessed and modified from both the JavaScript and UI threads. It returns an object with a .value property that holds the current value. Shared values are the foundation of any worklet-based animation, serving as the reactive state that animations read and modify.

const offsetX = useSharedValue(0);
const offsetY = useSharedValue(0);
const scale = useSharedValue(1);

Shared values can hold any JavaScript value--numbers for positions and scales, colors for backgrounds and text, or objects for complex state. When a shared value changes, any useAnimatedStyle or useDerivedValue that references it automatically re-runs on the UI thread.

useAnimatedStyle: Connecting Shared Values to Styles

The useAnimatedStyle hook creates a style object that depends on shared values and runs on the UI thread. The function you pass to useAnimatedStyle should return a style object, and any shared values accessed within this function create a dependency that triggers style updates when those values change.

const animatedStyle = useAnimatedStyle(() => ({
 transform: [
 { translateX: offsetX.value },
 { translateY: offsetY.value },
 { scale: scale.value }
 ],
 opacity: opacity.value
}));

useDerivedValue: Computed Shared Values

The useDerivedValue hook computes a new shared value from one or more other shared values. It runs on the UI thread and only recalculates when its dependencies change, making it more efficient than computing values inside useAnimatedStyle for complex calculations.

const width = useDerivedValue(() => {
 return Math.min(Math.max(offset.value * 2, 100), 500);
});

const animatedStyle = useAnimatedStyle(() => ({
 width: width.value
}));

useAnimatedScrollHandler: Scroll-Linked Animations

The useAnimatedScrollHandler hook creates an event handler for ScrollView scrolling that runs on the UI thread. This is essential for scroll-linked effects like collapsing headers, parallax backgrounds, and sticky elements.

const scrollY = useSharedValue(0);

const scrollHandler = useAnimatedScrollHandler({
 onScroll: (event) => {
 scrollY.value = event.contentOffset.y;
 },
});

return (
 <Animated.ScrollView
 onScroll={scrollHandler}
 scrollEventThrottle={16}
 >
 {/* Scrollable content */}
 </Animated.ScrollView>
);

Animation Functions: Controlling Motion

withSpring: Physics-Based Motion

The withSpring function creates animations that follow physics-based motion, simulating the behavior of a real spring. This produces more natural, organic-feeling animations than fixed-duration animations. Spring animations feel better because they account for velocity and resistance, making interactions feel more responsive to user input.

offsetX.value = withSpring(0, {
 damping: 15,
 stiffness: 150,
 mass: 1,
});

The damping parameter controls how quickly the oscillation stops, stiffness determines the spring's resistance to displacement, and mass affects how much force is needed to move the spring.

withTiming: Controlled Duration

The withTiming function creates animations that complete in a specified duration using a timing function. This is useful when you need precise control over animation timing, such as synchronized sequences.

opacity.value = withTiming(0, {
 duration: 300,
 easing: Easing.inOut(Easing.ease)
});

Timing animations use easing functions to control the pace of change. Common options include linear (constant speed), ease-in (starts slow, accelerates), ease-out (starts fast, decelerates), and ease-in-out.

withDecay: Natural Slowdown

The withDecay function creates animations that start with an initial velocity and gradually slow down, simulating friction. This is perfect for implementing swipe gestures where the animation should continue in the direction of the swipe and naturally come to a stop.

offsetX.value = withDecay({
 velocity: event.velocityX,
 deceleration: 0.5,
});

Composing Animations: withSequence and withRepeat

The withSequence function chains multiple animations together, running them in order. The withRepeat function repeats an animation a specified number of times or infinitely.

const pulse = withSequence(
 withSpring(1.2, { damping: 15 }),
 withSpring(1.0, { damping: 15 })
);

scale.value = withRepeat(pulse, -1, true);

Setting numberOfRepeats to -1 creates an infinite loop, and setting reverse to true makes the animation oscillate back and forth.

Gesture-Driven Animations

Setting Up Gesture Handler

Before using gestures with Reanimated, you need to wrap your app with GestureHandlerRootView and install react-native-gesture-handler:

import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
 return (
 <GestureHandlerRootView style={{ flex: 1 }}>
 {/* Your app content */}
 </GestureHandlerRootView>
 );
}

Creating Gesture-Driven Animations

The Gesture API provides gesture recognizers that work seamlessly with Reanimated shared values:

const pan = Gesture.Pan()
 .onChange((event) => {
 offsetX.value += event.changeX;
 offsetY.value += event.changeY;
 })
 .onEnd(() => {
 offsetX.value = withSpring(0);
 offsetY.value = withSpring(0);
 });

return (
 <GestureDetector gesture={pan}>
 <Animated.View style={[styles.box, animatedStyle]} />
 </GestureDetector>
);

Scroll-Linked Animations

Scroll-linked animations create effects that respond to scroll position, such as collapsing headers:

const headerStyle = useAnimatedStyle(() => {
 const height = interpolate(
 scrollY.value,
 [0, 150],
 [200, 60],
 'clamp'
 );
 return { height };
});

return (
 <View style={{ flex: 1 }}>
 <Animated.View style={[styles.header, headerStyle]}>
 <Text>Header</Text>
 </Animated.View>
 <Animated.ScrollView
 onScroll={scrollHandler}
 scrollEventThrottle={16}
 >
 {/* Content */}
 </Animated.ScrollView>
 </View>
);

For mobile apps that require intuitive touch interactions, combining Reanimated with professional mobile development services ensures your animations feel natural and responsive.

CSS Animations in Reanimated 4

Reanimated 4 introduces CSS-like declarative animations that complement the worklet-based API. These animations provide a simpler approach for state-driven transitions.

Basic Transitions

CSS-style transitions automatically animate property changes when state changes:

<Animated.View style={{
 width: expanded ? 300 : 200,
 height: expanded ? 200 : 100,
 backgroundColor: expanded ? '#4ade80' : '#86efac',
 transitionProperty: ['width', 'height', 'backgroundColor'],
 transitionDuration: 300,
 transitionTimingFunction: 'ease-in-out',
}}>
 <Text>Tap to toggle</Text>
</Animated.View>

The transitionProperty array specifies which properties to animate. When expanded changes, Reanimated automatically interpolates between values over 300 milliseconds.

Keyframe Animations

Keyframe animations define multi-step sequences with precise control over each stage:

const pulseAnimation = {
 '0%': { scale: 1, opacity: 1 },
 '50%': { scale: 1.05, opacity: 0.8 },
 '100%': { scale: 1, opacity: 1 },
};

<Animated.View style={{
 animationName: pulseAnimation,
 animationDuration: 2000,
 animationIterationCount: 'infinite',
}} />

Built-In Animation Primitives

Reanimated includes pre-built animations for common entrance and exit effects:

import { FadeIn, FadeOut, SlideInRight, ZoomIn } from 'react-native-reanimated';

<Animated.View entering={FadeIn.duration(300)} exiting={FadeOut.duration(200)}>
 <Text>Fade in/out</Text>
</Animated.View>

These built-in animations save you from writing manual configurations for standard patterns.

Performance Optimization Techniques

Even with animations running on the UI thread, poor animation code can still cause frame drops. These optimization techniques ensure consistent 60 FPS.

Memoize Animation Objects

Creating new animation objects inside component renders wastes memory and processing time. Define animations once outside the component or memoize them:

// Good: Create once and reuse
const fadeIn = FadeIn.duration(300);
{items.map(item => (
 <Animated.View entering={fadeIn} key={item.id}>
 {item.content}
 </Animated.View>
))}

Prefer useDerivedValue for Expensive Calculations

Complex calculations inside useAnimatedStyle run every frame even if dependencies haven't changed. useDerivedValue recalculates only when dependencies change:

// Better: Calculate once when offset changes
const width = useDerivedValue(() =>
 Math.min(Math.max(offset.value * 2, 100), 500)
);

const animatedStyle = useAnimatedStyle(() => ({
 width: width.value
}));

Batch UI Thread Updates

When updating multiple shared values, each update can trigger separate re-renders. Batch updates using runOnUI:

runOnUI(() => {
 'worklet';
 scale.value = withSpring(1.2);
 opacity.value = withSpring(0.8);
})();

Use Transforms Over Layout Properties

Animating layout properties like width or height forces React Native to recalculate element positions. Transform properties are hardware-accelerated:

// Good: Hardware-accelerated
transform: [{ scaleX: scale.value }]

// Avoid: Triggers layout recalculation
width: withSpring(newWidth)

Optimizing animation performance is crucial for apps that need to feel responsive on a wide range of devices, from flagship phones to older budget devices.

Best Practices and Common Pitfalls

Always Use GestureHandlerRootView

Without GestureHandlerRootView wrapping your app, gesture animations won't work. This is the most common setup issue for beginners. Place it at your app's root level, wrapping all other components.

Clear Cache After Configuration Changes

After modifying babel.config.js or installing new packages, always clear the Metro bundler cache:

npx expo start -c
# or for React Native CLI
npm start -- --reset-cache

Verify New Architecture Compatibility

Reanimated 4 requires React Native's New Architecture (Fabric). Ensure your project meets this requirement--React Native 0.76+ has it enabled by default. Check iOS Podfile for ENV['RCT_NEW_ARCH_ENABLED'] = '1' and android/gradle.properties for newArchEnabled=true.

Debug with Performance Monitor

Enable React Native's Performance Monitor (shake device, select "Show Perf Monitor") to verify animations run at 60 FPS. Watch the UI thread FPS--JS thread FPS affects renders, not animations.

Test on Real Devices

Simulators don't accurately represent real-world performance. Always test animations on actual devices, particularly on lower-end hardware where performance issues become more apparent.

Following these best practices helps ensure your animations perform well across all devices and provides a consistent user experience.

Practical Examples: Common UI Patterns

Collapsible Header

A header that shrinks as users scroll maximizes content space while keeping navigation accessible:

const scrollY = useSharedValue(0);
const HEADER_MAX = 200;
const HEADER_MIN = 60;

const scrollHandler = useAnimatedScrollHandler({
 onScroll: (event) => {
 scrollY.value = event.contentOffset.y;
 },
});

const headerStyle = useAnimatedStyle(() => ({
 height: interpolate(
 scrollY.value,
 [0, HEADER_MAX - HEADER_MIN],
 [HEADER_MAX, HEADER_MIN],
 'clamp'
 ),
}));

return (
 <View style={{ flex: 1 }}>
 <Animated.View style={[styles.header, headerStyle]}>
 <Text>My App</Text>
 </Animated.View>
 <Animated.ScrollView
 onScroll={scrollHandler}
 scrollEventThrottle={16}
 >
 {/* Content */}
 </Animated.ScrollView>
 </View>
);

Bottom Sheet

A draggable bottom sheet for action menus and additional content:

const translateY = useSharedValue(300);
const context = useSharedValue({ y: 0 });

const pan = Gesture.Pan()
 .onStart(() => {
 context.value = { y: translateY.value };
 })
 .onChange((event) => {
 translateY.value = Math.max(
 event.translationY + context.value.y,
 -300
 );
 })
 .onEnd((event) => {
 if (event.velocityY > 500) {
 translateY.value = withSpring(300); // Dismiss
 } else if (translateY.value > -100) {
 translateY.value = withSpring(-50); // Collapsed
 } else {
 translateY.value = withSpring(-300); // Expanded
 }
 });

const animatedStyle = useAnimatedStyle(() => ({
 transform: [{ translateY: translateY.value }],
}));

return (
 <GestureDetector gesture={pan}>
 <Animated.View style={[styles.bottomSheet, animatedStyle]}>
 <View style={styles.handle} />
 {children}
 </Animated.View>
 </GestureDetector>
);

Swipe-to-Delete

A list item that slides and collapses when swiped left:

const translateX = useSharedValue(0);
const itemHeight = useSharedValue(60);

const pan = Gesture.Pan()
 .activeOffsetX([-10, 10])
 .onChange((event) => {
 if (event.translationX < 0) {
 translateX.value = event.translationX;
 }
 })
 .onEnd(() => {
 if (translateX.value < -100) {
 translateX.value = withTiming(-500, { duration: 200 });
 itemHeight.value = withTiming(0, { duration: 200 }, () => {
 runOnJS(onDelete)();
 });
 } else {
 translateX.value = withSpring(0);
 }
 });

const animatedStyle = useAnimatedStyle(() => ({
 transform: [{ translateX: translateX.value }],
 height: itemHeight.value,
}));

return (
 <GestureDetector gesture={pan}>
 <Animated.View style={[styles.item, animatedStyle]}>
 {children}
 </Animated.View>
 </GestureDetector>
);

These patterns form the foundation of modern mobile UI interactions and can be adapted for countless use cases in your applications.

Conclusion

React Native Reanimated represents a powerful toolkit for building high-performance mobile animations. The library's dual approach--CSS animations for declarative simplicity and worklets for imperative control--gives developers the flexibility to handle any animation scenario while maintaining 60 FPS performance.

The key to mastering Reanimated lies in understanding when to use each approach. For state-driven animations like modals appearing, accordions expanding, or colors transitioning, CSS animations provide the simplest path. For gesture-driven interactions, scroll-linked effects, and animations requiring real-time control, worklets deliver the precision and performance needed.

Performance optimization should be considered from the start--memoizing animations, batching updates, using transforms over layout properties, and leveraging useDerivedValue for complex calculations ensures your animations stay smooth even during heavy application usage.

As React Native applications increasingly compete with native apps on user experience, animations play a crucial role in creating the polished, responsive interfaces users expect. React Native Reanimated provides the foundation for achieving that polish, transforming good applications into great ones through fluid, performant motion.

If you're building React Native applications and want to implement these animation patterns effectively, our web development team has extensive experience creating smooth, performant mobile experiences that delight users.

Ready to Build Smoother Mobile Experiences?

Our team specializes in creating fluid, performant mobile applications using React Native and modern animation libraries.