React Native Reanimated 4: A Practical Guide to CSS Animations, Transitions, and Worklets

Reanimated 4 brings CSS Animations and Transitions to React Native alongside the proven worklet engine. Learn when to use each approach with practical code examples, migration tips from v3, and performance best practices.

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.View style 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 (like display) 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 withDecay or 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

  1. Install react-native-worklets — This is now a peer dependency. Add it to your project alongside react-native-reanimated.

  2. Enable the New Architecture — If you haven't already, migrate to Fabric. Reanimated 4 no longer supports Paper.

  3. Rename useScrollViewOffset to useScrollOffset — This is a direct rename with no API change.

  4. Replace useAnimatedGestureHandler — This was deprecated in Reanimated 3 and has been removed in 4. Migrate to the Gesture Handler 2 API with Gesture.Pan(), Gesture.Pinch(), etc.

  5. Update withSpring parameters — Replace restDisplacementThreshold and restSpeedThreshold with the new energyThreshold parameter.

  6. Remove duplicate Babel plugins — If you have both react-native-reanimated/plugin and react-native-worklets/plugin in 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 the withSpring threshold 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 width and height causes layout recalculation. When possible, use transform (scale, translate) instead — transforms are GPU-composited and avoid layout thrashing.

  • Use useAnimatedStyle sparingly. Each useAnimatedStyle hook 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 setState call so Reanimated can process them together.

  • Use cancelAnimation to 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:

  1. Layout animation (FadeIn/FadeOut) for mount/unmount transitions.
  2. Worklet gesture for swipe-to-dismiss with spring physics.
  3. 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:

ScenarioRecommended Approach
Button press color changeCSS Transition
Expanding/collapsing accordionCSS Transition
Loading spinnerCSS Animation (keyframes)
Skeleton shimmer placeholderCSS Animation (or presets library)
Drag-and-drop reorderingWorklet + Gesture Handler
Pull-to-refresh custom animationWorklet + scroll offset
Parallax scroll headerWorklet + interpolate
Swipe-to-dismiss cardWorklet + Gesture Handler
Tab indicator slideCSS Transition
Notification entrance/exitLayout Animation (entering/exiting)
Complex multi-step choreographyWorklet + 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.

About the Author Editorial Team

Our team of expert writers and editors.