Reanimated 4 changed how React Native animations are written. Instead of creating shared values and writing worklets for every fade and slide, you can now describe animations the way you would on the web, with CSS keyframes and transitions, right in the style prop. Animations still run on the UI thread, so they stay smooth at 60–120 fps.
This guide shows the new CSS API with copy-paste examples and explains when you still need the classic worklet API.
Install Reanimated 4 in Expo
npx expo install react-native-reanimated react-native-worklets
Two things to know:
- New Architecture only. Reanimated 4 requires Fabric. Expo SDK 55+ uses the New Architecture by default, so most Expo apps are already set.
- Worklets moved packages. The worklet runtime now lives in
react-native-worklets. Expo's Babel preset includes its plugin automatically. In a bare React Native app, addreact-native-worklets/pluginas the last plugin inbabel.config.js.
Your first CSS animation: fade in
Use any Animated component and put the animation in its style:
import Animated from "react-native-reanimated"
export function FadeIn({ children }: { children: React.ReactNode }) {
return (
<Animated.View
style={{
animationName: {
from: { opacity: 0, transform: [{ translateY: 12 }] },
to: { opacity: 1, transform: [{ translateY: 0 }] },
},
animationDuration: "400ms",
animationTimingFunction: "ease-out",
}}
>
{children}
</Animated.View>
)
}
No hooks and no shared values. The animation runs when the component mounts.
Reusable keyframes
Define keyframes once and reuse them anywhere. Percent keys work just like CSS:
const pulse = {
"0%": { transform: [{ scale: 1 }] },
"50%": { transform: [{ scale: 1.08 }] },
"100%": { transform: [{ scale: 1 }] },
}
<Animated.View
style={{
animationName: pulse,
animationDuration: "1.2s",
animationIterationCount: "infinite",
animationTimingFunction: "ease-in-out",
}}
/>
You need at least one keyframe. If you omit the first one, Reanimated uses the view's current style as the starting point.
Run multiple animations at once
Pass arrays to combine animations, each with its own duration:
<Animated.View
style={{
animationName: [fadeIn, slideUp, wiggle],
animationDuration: ["300ms", "500ms", "1s"],
}}
/>
If two animations change the same property, the later one in the array wins.
CSS transitions: animate style changes automatically
Transitions are the easiest win in Reanimated 4. Tell a view which properties to transition, then change its style normally. Reanimated animates between the old and new values:
import { useState } from "react"
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"
export function ExpandingCard() {
const [open, setOpen] = useState(false)
return (
<Pressable onPress={() => setOpen((o) => !o)}>
<Animated.View
style={{
height: open ? 220 : 96,
backgroundColor: open ? "#7c3aed" : "#18181b",
borderRadius: open ? 28 : 16,
transitionProperty: ["height", "backgroundColor", "borderRadius"],
transitionDuration: 300,
transitionTimingFunction: "ease-in-out",
}}
/>
</Pressable>
)
}
You need both transitionProperty and transitionDuration for a transition to run. Use transitionProperty: "all" to animate every animatable change.
Practical recipes
Skeleton shimmer (loading placeholders):
<Animated.View
style={{
height: 16,
borderRadius: 8,
backgroundColor: "#e4e4e7",
animationName: { "0%": { opacity: 1 }, "50%": { opacity: 0.4 }, "100%": { opacity: 1 } },
animationDuration: "1.4s",
animationIterationCount: "infinite",
}}
/>
Spinner:
<Animated.View
style={{
animationName: { to: { transform: [{ rotate: "360deg" }] } },
animationDuration: "900ms",
animationIterationCount: "infinite",
animationTimingFunction: "linear",
}}
/>
Toggle-driven tab indicator: set left or transform from state and add transitionProperty: "transform".
When to still use shared values and worklets
CSS animations are declarative: they react to mounts and state changes. Some animations need to track a value on every frame, and those still belong to the classic API:
- Gesture-driven UI such as bottom sheets, swipe-to-dismiss, and drag-and-drop (
react-native-gesture-handlerplususeSharedValue). - Scroll-linked effects like collapsing headers and parallax (
useAnimatedScrollHandler). - Physics that responds to velocity, like springs that follow a flick.
import { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
const offset = useSharedValue(0)
const style = useAnimatedStyle(() => ({
transform: [{ translateY: withSpring(offset.value) }],
}))
A good rule: CSS for state, worklets for gestures and scroll. Both APIs work side by side in the same app.
Performance tips
- Animate
transformandopacitywhen you can. They're the cheapest properties. - Avoid animating layout props (
width,height) on long lists. Usetransform: scaleinstead. - Keep keyframe objects outside the component (or memoize them) so they aren't recreated on each render.
- Test animations on a low-end Android device in a release build. Dev builds are much slower.
See Reanimated in production components
Our free Native Bottom Sheet is a real example of the worklet side of Reanimated 4: snap points, velocity-projected flicks, rubber banding, and a blur backdrop that ramps with sheet position, all on the UI thread. For effects that don't need Reanimated at all, look at the native-driver animations in Expo Confetti and Gradient Background.
Next, pair motion with touch feedback in our expo-haptics guide, or see where Reanimated fits among the best React Native libraries of 2026.