React NativeReanimatedAnimationsExpo

Reanimated 4 CSS Animations in React Native: Complete Guide

Animate React Native apps the CSS way with Reanimated 4 — keyframe animations, transitions, timing, iteration, and when to still use shared values and worklets. Copy-paste examples for Expo, New Architecture required.

DeveloperMill4 min read

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, add react-native-worklets/plugin as the last plugin in babel.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-handler plus useSharedValue).
  • 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 transform and opacity when you can. They're the cheapest properties.
  • Avoid animating layout props (width, height) on long lists. Use transform: scale instead.
  • 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.

Frequently asked questions

What is new in Reanimated 4?

Reanimated 4 adds a declarative, CSS-compatible animation API. You can define keyframe animations with animationName and animationDuration, and animate style changes with transitionProperty and transitionDuration, directly in the style prop of an Animated component — no shared values or worklets required for common cases.

Does Reanimated 4 work on the old architecture?

No. Reanimated 4.x works only with React Native's New Architecture (Fabric). Apps still on the old architecture must stay on Reanimated 3, which is no longer actively maintained. Expo SDK 55 and later run on the New Architecture by default.

Do I still need worklets in Reanimated 4?

For state-driven and declarative animations, no — CSS animations and transitions cover them. You still use shared values, useAnimatedStyle, and worklets for gesture-driven animations, scroll-linked effects, and anything that reacts to values frame by frame. Worklets now live in the separate react-native-worklets package.

How do I install Reanimated 4 in Expo?

Run `npx expo install react-native-reanimated react-native-worklets`. The Expo Babel preset already includes the worklets Babel plugin, so no babel.config.js changes are needed in a standard Expo project.

Build it faster with a template

Production-ready Expo React Native templates with the patterns from this post already wired up.

Browse templates