Why Reanimated 4 Is a Big Deal
If you've built any non-trivial React Native app, you've dealt with animations. Maybe a bottom sheet that slides up, a card that flips on tap, a loading spinner — the usual suspects. And if you've been doing this for any length of time, you know that getting animations right (smooth, 60 FPS, gesture-driven) has always been one of React Native's roughest edges.
React Native Reanimated changed that story. But even Reanimated 3, as powerful as it was, required you to think in worklets, shared values, and UI-thread callbacks for every single animation, no matter how simple. Want a button to change color on press? Shared value. Want a card to fade in on mount? Layout animation with custom configuration. It worked, sure, but there was a lot of ceremony for common cases.
Reanimated 4 fixes this. It introduces a completely new layer on top of the existing worklet engine: CSS Animations and CSS Transitions. If you've ever written a CSS transition on the web, you already know how to use half of Reanimated 4's new API. And for the complex stuff — gesture-driven interactions, scroll-linked animations, orchestrated sequences — worklets and shared values are still right there, better than ever.
This guide covers everything you need to know: installation, the new CSS APIs, when to use worklets versus CSS, layout animations, gesture integration, performance tuning, and migration from v3. So, let's get into it.
What's New in Reanimated 4
Reanimated 4.0 dropped as the first stable version in mid-2025, and it's honestly the biggest API addition since Reanimated 2 introduced worklets. Here's what changed at a high level:
- CSS Animations API — Define keyframe-based animations using a familiar CSS syntax, applied directly to
Animated.Viewstyle props. - CSS Transitions API — Automatically animate property changes on state updates. Specify which properties to transition, the duration, easing, and delay — and let Reanimated handle the rest.
- Worklets extracted to
react-native-worklets— The worklet runtime has been moved to its own package, making it available to non-animation libraries and accelerating independent development. - New Architecture only — Reanimated 4.x drops support for the Legacy Architecture (Paper) entirely. You'll need React Native 0.76 or newer with Fabric enabled.
- Backward compatibility — All existing Reanimated 2/3 APIs (
useSharedValue,useAnimatedStyle,withTiming,withSpring, layout animations) continue to work with minimal changes.
The philosophy is pretty clear: use CSS for the 80% of animations that are state-driven and declarative, and use worklets for the 20% that need frame-level control.
Installation and Setup
Expo Projects
If you're on Expo SDK 52 or later, adding Reanimated 4 is about as straightforward as it gets:
npx expo install react-native-reanimated react-native-worklets
That's it. The babel-preset-expo automatically configures the Reanimated Babel plugin, so no manual Babel configuration is needed. Just make sure you're running the New Architecture — if you created your project with a recent Expo SDK, it's already enabled by default.
Bare React Native Projects
For bare React Native projects (0.76+), install both packages:
npm install react-native-reanimated react-native-worklets
# or
yarn add react-native-reanimated react-native-worklets
Then add the Babel plugin to your babel.config.js:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-reanimated/plugin'],
};
Important: The react-native-worklets/plugin is already included inside react-native-reanimated/plugin. Don't add both — it causes a conflict. If you had the worklets plugin listed separately, just remove it.
Web Support
For web support (React Native Web or Expo Web), install one additional Babel plugin:
npm install @babel/plugin-proposal-export-namespace-from
And add it to your Babel config alongside the Reanimated plugin. The react-native-worklets/plugin should be listed last.
CSS Transitions: The Simplest Way to Animate
CSS Transitions are the quickest path to polished UI. You tell Reanimated which properties to watch, how long the transition should take, and then just update your state. That's it. Reanimated handles the interpolation on the UI thread at full frame rate.
Basic Transition Example
import { useState } from 'react';
import { Pressable, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';
function ExpandableCard() {
const [expanded, setExpanded] = useState(false);
return (
<Pressable onPress={() => setExpanded(!expanded)}>
<Animated.View
style={[
styles.card,
{
width: expanded ? 300 : 150,
height: expanded ? 200 : 100,
backgroundColor: expanded ? '#6c5ce7' : '#74b9ff',
borderRadius: expanded ? 16 : 8,
transitionProperty: ['width', 'height', 'backgroundColor', 'borderRadius'],
transitionDuration: 400,
transitionTimingFunction: 'ease-in-out',
},
]}
/>
</Pressable>
);
}
const styles = StyleSheet.create({
card: {
alignSelf: 'center',
marginTop: 40,
},
});
That's the entire implementation. No useSharedValue, no useAnimatedStyle, no withTiming wrapper. You define which properties to transition, set a duration, and change state. Reanimated picks up the change and smoothly interpolates every listed property on the UI thread. If you're coming from web development, this probably feels refreshingly familiar.
Transition Properties Reference
The CSS Transitions API accepts these style props on any Animated component:
transitionProperty— An array of property names to animate (e.g.,['width', 'opacity', 'transform']).transitionDuration— Duration in milliseconds (number) or an array of durations mapped to each property.transitionDelay— Delay before the transition starts, in milliseconds.transitionTimingFunction— Easing curve. Accepts strings like'ease','ease-in','ease-out','ease-in-out','linear', or a custom cubic bezier.transitionBehavior— Controls how discrete properties (likedisplay) transition.
When to Use Transitions
Transitions are perfect for UI polish that reacts to state changes:
- Show/hide toggles (opacity, height)
- Color changes on selection or focus
- Size adjustments (expanding cards, growing buttons)
- Tab indicator slides
- Position nudges on layout changes
If the animation is triggered by a state update and doesn't need frame-by-frame manual control, reach for a transition first. You'll save yourself a surprising amount of boilerplate.
CSS Animations: Keyframes in React Native
For animations that loop, have multiple stages, or need to play on mount without a state change, CSS Animations are what you want. They work like @keyframes in web CSS — you define named keyframe steps and attach them to a component.
Pulse Animation Example
import Animated from 'react-native-reanimated';
import { StyleSheet } from 'react-native';
const pulse = {
from: {
transform: [{ scale: 1 }],
opacity: 1,
},
'50%': {
transform: [{ scale: 1.08 }],
opacity: 0.7,
},
to: {
transform: [{ scale: 1 }],
opacity: 1,
},
};
function PulsingDot() {
return (
<Animated.View
style={[
styles.dot,
{
animationName: pulse,
animationDuration: 1500,
animationIterationCount: 'infinite',
animationTimingFunction: 'ease-in-out',
},
]}
/>
);
}
const styles = StyleSheet.create({
dot: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: '#e74c3c',
},
});
Multi-Step Keyframe Animation
You can define as many keyframe steps as you need. Here's a more complex entrance animation with a subtle bounce effect:
const slideInBounce = {
'0%': {
transform: [{ translateY: -100 }],
opacity: 0,
},
'60%': {
transform: [{ translateY: 10 }],
opacity: 1,
},
'80%': {
transform: [{ translateY: -5 }],
},
'100%': {
transform: [{ translateY: 0 }],
},
};
function WelcomeBanner() {
return (
<Animated.View
style={[
styles.banner,
{
animationName: slideInBounce,
animationDuration: 800,
animationFillMode: 'forwards',
animationTimingFunction: 'ease-out',
},
]}
>
<Animated.Text style={styles.bannerText}>
Welcome back!
</Animated.Text>
</Animated.View>
);
}
CSS Animation Properties Reference
animationName— A keyframes object defining the animation steps.animationDuration— Duration in milliseconds.animationDelay— Delay before the animation starts.animationIterationCount— Number of times to repeat, or'infinite'.animationDirection—'normal','reverse','alternate', or'alternate-reverse'.animationFillMode—'none','forwards','backwards', or'both'.animationTimingFunction— Same easing options as transitions.
Using the CSS Animation Presets Library
Here's something nice — Software Mansion provides an official companion library, react-native-css-animations, with ready-made animation presets you can drop in immediately:
npm install react-native-css-animations
The library includes common animations like spin, ping, pulse, bounce, and shimmer. Using them is dead simple — just spread the preset into your style:
import { spin, pulse, shimmer, bounce } from 'react-native-css-animations';
import Animated from 'react-native-reanimated';
// Spinning loader
function Loader() {
return <Animated.View style={[styles.spinner, spin]} />;
}
// Skeleton loading placeholder
function SkeletonCard() {
return <Animated.View style={[styles.skeleton, shimmer]} />;
}
// Notification badge
function NotificationBadge({ count }) {
return (
<Animated.View style={[styles.badge, count > 0 && ping]}>
<Animated.Text style={styles.badgeText}>{count}</Animated.Text>
</Animated.View>
);
}
These presets are just objects with the standard CSS animation properties, so you can override individual values by spreading and then overriding:
<Animated.View style={[styles.icon, { ...spin, animationDuration: 2000 }]} />
Worklets and Shared Values: When You Need Full Control
CSS animations and transitions cover the majority of use cases, but some animations just can't be expressed declaratively. When you need to respond to every frame of a gesture or tie an animation to scroll position, worklets are still the way to go.
When to Use Worklets Instead of CSS
- Gesture-driven animations — Dragging, swiping, pinching, where the animation responds to continuous touch input frame by frame.
- Scroll-linked animations — Parallax headers, sticky elements, progress indicators tied to scroll position.
- Physics-based interactions — Momentum, flinging, snap points that need
withDecayor custom spring physics. - Orchestrated sequences — Complex chains of animations that depend on each other's completion.
- Screen transitions — Custom navigation transitions that coordinate multiple elements.
Gesture-Driven Drag Example
Here's a draggable card that snaps back to its original position with a spring animation when released. This is a textbook case where worklets shine:
import { StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const scale = useSharedValue(1);
const pan = Gesture.Pan()
.onBegin(() => {
scale.value = withSpring(1.05);
})
.onChange((event) => {
translateX.value += event.changeX;
translateY.value += event.changeY;
})
.onFinalize(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
scale.value = withSpring(1);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]} />
</GestureDetector>
);
}
const styles = StyleSheet.create({
card: {
width: 200,
height: 120,
backgroundColor: '#0984e3',
borderRadius: 12,
alignSelf: 'center',
marginTop: 100,
},
});
This animation responds to every finger movement in real time on the UI thread. There's no way to express this with a CSS transition because the animation isn't driven by a state change — it's driven by continuous gesture events at 60–120 FPS.
Scroll-Linked Parallax Header
import Animated, {
useSharedValue,
useAnimatedStyle,
useScrollOffset,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
import { useRef } from 'react';
function ParallaxHeader() {
const scrollRef = useRef(null);
const scrollOffset = useScrollOffset(scrollRef);
const headerStyle = useAnimatedStyle(() => {
const translateY = interpolate(
scrollOffset.value,
[0, 200],
[0, -100],
Extrapolation.CLAMP
);
const opacity = interpolate(
scrollOffset.value,
[0, 150],
[1, 0],
Extrapolation.CLAMP
);
return {
transform: [{ translateY }],
opacity,
};
});
return (
<>
<Animated.View style={[styles.header, headerStyle]}>
<Animated.Text style={styles.headerText}>Parallax</Animated.Text>
</Animated.View>
<Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}>
{/* scroll content */}
</Animated.ScrollView>
</>
);
}
Note the use of useScrollOffset — this is the renamed version of useScrollViewOffset from Reanimated 3. If you're migrating, this is one of the few API renames you'll run into.
Layout Animations: Entering, Exiting, and Layout Transitions
Reanimated's layout animation system lets you animate components as they mount, unmount, or change position in the layout. These work alongside the new CSS APIs and remain a core part of Reanimated 4.
Entering and Exiting Animations
import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft } from 'react-native-reanimated';
function AnimatedListItem({ item, onRemove }) {
return (
<Animated.View
entering={SlideInRight.duration(400).springify()}
exiting={SlideOutLeft.duration(300)}
style={styles.listItem}
>
<Text>{item.title}</Text>
<Pressable onPress={() => onRemove(item.id)}>
<Text>Remove</Text>
</Pressable>
</Animated.View>
);
}
function NotificationToast({ visible, message }) {
if (!visible) return null;
return (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={styles.toast}
>
<Text style={styles.toastText}>{message}</Text>
</Animated.View>
);
}
The entering and exiting props accept any of Reanimated's built-in animation presets (FadeIn, FadeOut, SlideInRight, SlideOutLeft, ZoomIn, BounceIn, and many more). You can chain modifiers like .duration(), .delay(), .springify(), and .easing() to customize the behavior.
Layout Transitions
When components change position due to siblings being added, removed, or resized, layout transitions animate the repositioning smoothly:
import Animated, { LinearTransition } from 'react-native-reanimated';
function ReorderableList({ items }) {
return (
<Animated.View style={styles.container}>
{items.map((item) => (
<Animated.View
key={item.id}
layout={LinearTransition.springify()}
style={styles.item}
>
<Text>{item.name}</Text>
</Animated.View>
))}
</Animated.View>
);
}
Combining CSS and Worklet Approaches
One of Reanimated 4's greatest strengths is that the CSS and worklet APIs aren't mutually exclusive. You can use both on the same component — and honestly, this is where things get really interesting.
Here's a practical example: a card that has a CSS transition for its background color and border, but uses worklets for gesture-driven dragging:
import { useState } from 'react';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function InteractiveCard() {
const [selected, setSelected] = useState(false);
const translateX = useSharedValue(0);
const swipe = Gesture.Pan()
.onChange((e) => {
translateX.value = e.translationX;
})
.onFinalize(() => {
if (Math.abs(translateX.value) > 150) {
// swiped far enough — trigger action
}
translateX.value = withSpring(0);
});
const gestureStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={swipe}>
<Animated.View
style={[
styles.card,
gestureStyle,
{
// CSS Transition for state-driven changes
backgroundColor: selected ? '#00b894' : '#dfe6e9',
borderWidth: selected ? 2 : 0,
borderColor: '#00b894',
transitionProperty: ['backgroundColor', 'borderWidth', 'borderColor'],
transitionDuration: 300,
},
]}
onTouchEnd={() => setSelected(!selected)}
/>
</GestureDetector>
);
}
The CSS transition handles the color and border animation declaratively, while the worklet handles the swipe gesture imperatively. Each approach does what it's best at — and that's the real beauty of Reanimated 4's design.
Animation Functions Deep Dive
Even with the new CSS APIs, Reanimated's core animation functions remain essential for worklet-based code. Here's a quick reference for each.
withTiming
A duration-based animation with customizable easing. Default duration is 300ms.
import { withTiming, Easing } from 'react-native-reanimated';
// Simple
opacity.value = withTiming(1);
// With config
opacity.value = withTiming(1, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
// With callback
opacity.value = withTiming(1, { duration: 300 }, (finished) => {
if (finished) {
// Animation completed
}
});
withSpring
A physics-based spring animation. In Reanimated 4, the restDisplacementThreshold and restSpeedThreshold parameters have been replaced with a single energyThreshold — a welcome simplification if you ask me:
import { withSpring } from 'react-native-reanimated';
// Simple bounce-back
translateX.value = withSpring(0);
// Customized spring
translateX.value = withSpring(0, {
mass: 1,
stiffness: 150,
damping: 12,
// Reanimated 4: use energyThreshold instead of
// restDisplacementThreshold / restSpeedThreshold
energyThreshold: 0.01,
});
withDecay
Simulates momentum — the animation starts at a given velocity and gradually slows down. Great for flick-to-scroll type interactions:
import { withDecay } from 'react-native-reanimated';
// After a fling gesture
translateX.value = withDecay({
velocity: event.velocityX,
clamp: [-200, 200], // bounds
});
Combining with withSequence and withDelay
import { withSequence, withDelay, withTiming } from 'react-native-reanimated';
// Shake animation
translateX.value = withSequence(
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(-10, { duration: 50 }),
withTiming(0, { duration: 50 })
);
// Delayed entrance
opacity.value = withDelay(500, withTiming(1, { duration: 400 }));
Migrating from Reanimated 3.x to 4.x
The migration surface is deliberately small. Software Mansion designed Reanimated 4 to be backward compatible for the vast majority of existing code — and they did a good job of it. Here's what you actually need to change:
Required Changes
Install
react-native-worklets— This is now a peer dependency. Add it to your project alongsidereact-native-reanimated.Enable the New Architecture — If you haven't already, migrate to Fabric. Reanimated 4 no longer supports Paper.
Rename
useScrollViewOffsettouseScrollOffset— This is a direct rename with no API change.Replace
useAnimatedGestureHandler— This was deprecated in Reanimated 3 and has been removed in 4. Migrate to the Gesture Handler 2 API withGesture.Pan(),Gesture.Pinch(), etc.Update
withSpringparameters — ReplacerestDisplacementThresholdandrestSpeedThresholdwith the newenergyThresholdparameter.Remove duplicate Babel plugins — If you have both
react-native-reanimated/pluginandreact-native-worklets/pluginin your Babel config, remove the worklets one. It's already included.
What Doesn't Change
useSharedValue,useAnimatedStyle,useDerivedValue— all unchanged.withTiming,withSpring,withDecay,withSequence,withDelay,withRepeat— all unchanged (except thewithSpringthreshold rename).- Layout animations (
entering,exiting,layout) — all unchanged. interpolate,interpolateColor— all unchanged.
For most projects, migration takes under an hour. The biggest effort will be if you still have useAnimatedGestureHandler calls that need refactoring to the Gesture Handler 2 API — but honestly, you should've done that a while ago anyway.
Performance Considerations
Reanimated 4 runs all animations — both CSS and worklet-based — on the UI thread. This means your JavaScript thread can be completely blocked (processing a large list, making network calls, whatever) and animations will still run at full frame rate. That's kind of the whole point.
Tips for Optimal Performance
Prefer CSS transitions for simple state-driven animations. They have less overhead than creating shared values and animated styles for straightforward property changes.
Avoid animating layout-triggering properties unnecessarily. Animating
widthandheightcauses layout recalculation. When possible, usetransform(scale, translate) instead — transforms are GPU-composited and avoid layout thrashing.Use
useAnimatedStylesparingly. EachuseAnimatedStylehook creates a mapping that runs on every frame. If you have dozens of animated components, this adds up. CSS transitions don't have this overhead — they only run when a property actually changes.Batch state updates. If you're triggering multiple CSS transitions at once, batch your state updates into a single
setStatecall so Reanimated can process them together.Use
cancelAnimationto clean up. If a component unmounts while a worklet animation is running, cancel it to free resources:
import { useSharedValue, cancelAnimation } from 'react-native-reanimated';
import { useEffect } from 'react';
function AnimatedComponent() {
const opacity = useSharedValue(0);
useEffect(() => {
return () => cancelAnimation(opacity);
}, []);
// ...
}
Building a Real-World Animated Component
Let's tie everything together by building a notification card that combines multiple Reanimated 4 features — CSS transitions, layout animations, and gesture-driven dismiss. This is the kind of component you'd actually ship in a production app:
import { useState, useCallback } from 'react';
import { Text, Pressable, StyleSheet, View } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
FadeIn,
FadeOut,
runOnJS,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function SwipeableNotification({ message, type = 'info', onDismiss }) {
const translateX = useSharedValue(0);
const [read, setRead] = useState(false);
const handleDismiss = useCallback(() => {
onDismiss?.();
}, [onDismiss]);
const swipe = Gesture.Pan()
.onChange((e) => {
translateX.value = e.translationX;
})
.onFinalize((e) => {
if (Math.abs(translateX.value) > 120) {
translateX.value = withTiming(
translateX.value > 0 ? 400 : -400,
{ duration: 200 },
() => runOnJS(handleDismiss)()
);
} else {
translateX.value = withSpring(0);
}
});
const gestureStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const bgColor = type === 'success' ? '#00b894'
: type === 'warning' ? '#fdcb6e'
: type === 'error' ? '#e17055'
: '#74b9ff';
return (
<GestureDetector gesture={swipe}>
<Animated.View
entering={FadeIn.duration(300).springify()}
exiting={FadeOut.duration(200)}
style={[
styles.notification,
gestureStyle,
{
backgroundColor: bgColor,
opacity: read ? 0.6 : 1,
// CSS Transition for read state
transitionProperty: ['opacity'],
transitionDuration: 300,
},
]}
>
<Pressable onPress={() => setRead(true)} style={styles.content}>
<Text style={styles.message}>{message}</Text>
{!read && <View style={styles.unreadDot} />}
</Pressable>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
notification: {
marginHorizontal: 16,
marginVertical: 4,
padding: 16,
borderRadius: 12,
},
content: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
message: {
color: '#fff',
fontSize: 15,
fontWeight: '500',
flex: 1,
},
unreadDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#fff',
marginLeft: 8,
},
});
This single component demonstrates three different animation approaches working in harmony:
- Layout animation (
FadeIn/FadeOut) for mount/unmount transitions. - Worklet gesture for swipe-to-dismiss with spring physics.
- CSS transition for the read/unread opacity change.
Each approach handles the part it's best suited for. That's the pattern you want to follow in your own apps.
Decision Guide: CSS vs. Worklets
Here's a quick reference for choosing the right approach:
| Scenario | Recommended Approach |
|---|---|
| Button press color change | CSS Transition |
| Expanding/collapsing accordion | CSS Transition |
| Loading spinner | CSS Animation (keyframes) |
| Skeleton shimmer placeholder | CSS Animation (or presets library) |
| Drag-and-drop reordering | Worklet + Gesture Handler |
| Pull-to-refresh custom animation | Worklet + scroll offset |
| Parallax scroll header | Worklet + interpolate |
| Swipe-to-dismiss card | Worklet + Gesture Handler |
| Tab indicator slide | CSS Transition |
| Notification entrance/exit | Layout Animation (entering/exiting) |
| Complex multi-step choreography | Worklet + withSequence |
Wrapping Up
Reanimated 4 is, without question, the most significant release in the library's history. By adding CSS Animations and Transitions on top of the battle-tested worklet engine, it gives you the right tool for every animation scenario — without forcing you to choose one paradigm for everything.
For most apps, the new CSS APIs will handle 70–80% of animation needs with dramatically less code. And for the remaining interactive, gesture-driven, and scroll-linked animations, worklets and shared values are as powerful as ever.
The migration path from Reanimated 3 is smooth — a few renames, a new peer dependency, and you're done. If you're starting a new project, there's never been a better time to reach for Reanimated. And if you're on an existing project, the backward compatibility means you can adopt the new CSS APIs incrementally, one component at a time.
Start with transitions on your next UI polish pass. Replace a few useSharedValue + withTiming combos with transitionProperty and transitionDuration. You'll be surprised how much cleaner it feels — and your animations will still run at 120 FPS on the UI thread, exactly where they belong.