Architecture & Performance· 6 min read

60fps Animations with Reanimated: Patterns That Don't Drop Frames

React Native animations at 60fps with Reanimated 4: CSS transitions, gesture-driven motion, scroll effects, layout animations and real-time data visuals.

By

Full-Stack, Mobile & AI Engineer

An animation that stutters feels worse than no animation at all. Users can't name the problem, but they feel it: the swipe that lags behind the finger, the header that jerks as you scroll, the chart that freezes for a beat when new data arrives. At 60 frames per second you have about 16.7 milliseconds per frame; on 120Hz displays, half that.

React Native animations stay smooth when the motion runs on the UI thread and the JavaScript thread stays out of the way. Reanimated 4 makes that the default for most patterns. This guide covers the five animation patterns most apps need, with code, and the rules that keep each one at 60fps on real devices.

Key takeaways

  • Reanimated 4 is stable (since July 2025), requires the New Architecture, and adds CSS-style animations and transitions alongside worklets.
  • Use CSS transitions for state-driven changes (expand, toggle, fade) and worklets with shared values for gestures and scroll.
  • Animate transform and opacity wherever possible; animating layout properties forces layout work every frame.
  • Never call setState every frame. Keep per-frame values in shared values, and cross to JavaScript only when something meaningful finishes.
  • Respect reduced motion, and test on a mid-range Android phone in a release build.

How Reanimated keeps motion smooth

Reanimated runs animation logic in worklets, small functions that execute on the UI thread. Values that change every frame live in shared values, readable from both threads. Because the frame-by-frame work never waits for the JavaScript thread, a busy JavaScript thread (fetching data, rendering a list) doesn't stall the animation.

Reanimated 4 moved worklets into their own package, react-native-worklets, and added a declarative CSS-compatible API for animations and transitions. Shared-value animations work exactly as before, side by side with the new API.

Pattern 1: state-driven transitions (use CSS transitions)

When a component changes because state changed (a card expands, a button changes colour), describe the end state and let Reanimated animate the difference:

import Animated from "react-native-reanimated";

export function ExpandableCard({ expanded }: { expanded: boolean }) {
  return (
    <Animated.View
      style={{
        height: expanded ? 220 : 96,
        opacity: expanded ? 1 : 0.9,
        transitionProperty: ["height", "opacity"],
        transitionDuration: 250,
      }}
    />
  );
}

This is the simplest pattern and covers a lot of everyday UI.

Pattern 2: gesture-driven motion (use worklets)

For anything that follows a finger (swipe to dismiss, drag to reorder, bottom sheets), connect Gesture Handler to shared values:

import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";

export function SwipeCard() {
  const offset = useSharedValue(0);

  const pan = Gesture.Pan()
    .onChange((e) => {
      offset.value += e.changeX;
    })
    .onEnd(() => {
      offset.value = withSpring(0);
    });

  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }));

  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={[{ height: 120, borderRadius: 16 }, style]} />
    </GestureDetector>
  );
}

The gesture callbacks run on the UI thread, so the card tracks the finger with no JavaScript involvement until you decide something should happen (for example, deleting the item when it's swiped far enough).

Pattern 3: scroll-driven effects

Collapsing headers, parallax images and progress bars tied to scrolling should read the scroll position on the UI thread:

import Animated, { useAnimatedScrollHandler, useSharedValue, useAnimatedStyle, interpolate, Extrapolation } from "react-native-reanimated";

const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler((e) => {
  scrollY.value = e.contentOffset.y;
});
const headerStyle = useAnimatedStyle(() => ({
  transform: [{ translateY: interpolate(scrollY.value, [0, 120], [0, -60], Extrapolation.CLAMP) }],
}));

// <Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}> ... </Animated.ScrollView>

Pattern 4: layout animations

For items entering, leaving or moving in a list, Reanimated's layout animations handle the motion with one prop:

import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";

<Animated.View entering={FadeIn} exiting={FadeOut} layout={LinearTransition}>
  {/* row content */}
</Animated.View>

Use them sparingly in long lists, and test scroll performance after adding them.

Pattern 5: real-time data visuals

Charts and visualisations fed by live data (sensors, prices, audio levels) are the hardest case, because new data arrives many times per second. The rules:

  • Don't put every sample in React state. Write incoming values into shared values or a ring buffer, and let the drawing layer read them each frame.
  • Draw with a GPU-backed canvas such as React Native Skia for heat maps and dense charts, instead of hundreds of views.
  • Throttle to the display, not the sensor. If data arrives at 200Hz and the screen refreshes at 60Hz, render the latest state per frame rather than every sample.
From the field: For SuraSole Golf, I built the app for pressure-sensing smart insoles: real-time plantar-pressure data streamed over Bluetooth Low Energy from both feet, rendered as live heat maps and weight-shift charts at 60fps, and synced frame by frame against captured swing video. Keeping per-frame values out of React state is what makes that kind of screen possible.

Common animation examples

A few patterns cover most of what apps need. These use Reanimated 4:

Fade in or slide in when a view mounts:

import Animated, { FadeIn, SlideInLeft } from "react-native-reanimated";

<Animated.View entering={FadeIn.duration(300)}>{/* card */}</Animated.View>
<Animated.View entering={SlideInLeft.duration(300)}>{/* panel sliding in from the left */}</Animated.View>

Run code when an animation finishes, for example removing an item after it fades out:

import { withTiming } from "react-native-reanimated";
import { scheduleOnRN } from "react-native-worklets";

opacity.value = withTiming(0, { duration: 250 }, (finished) => {
  if (finished) scheduleOnRN(onHidden); // runs once, on the JavaScript side
});

A header that collapses on scroll is Pattern 3 above: read the scroll offset with useAnimatedScrollHandler and interpolate it into a transform.

Pull to refresh is best left to the platform's RefreshControl; build a custom animated version only when the design truly needs it, driving it from the scroll offset in the same way.

Animated vs Reanimated

React Native's built-in Animated API is fine for simple fades and slides, especially with useNativeDriver: true, which runs transform and opacity animations natively. It can't drive layout properties natively, struggles with gestures, and has no layout animations. Reanimated runs animation logic on the UI thread, integrates with Gesture Handler, and adds layout animations and CSS-style transitions. For anything interactive, Reanimated is the standard choice; Moti, Lottie, Rive and React Native Skia sit alongside it for declarative shortcuts, designer-made animations and custom drawing.

When animations don't work

  • Nothing animates at all: Reanimated 4 needs the New Architecture and a native rebuild after installing; in Expo, babel-preset-expo configures the worklets Babel plugin for you, while bare projects add react-native-worklets/plugin to their Babel config.
  • Layout or exiting animations don't play: the component must be an Animated component, and it has to stay mounted long enough to animate; a parent that unmounts at the same moment takes the child with it.
  • It stutters: check you're not updating React state on every frame, and test in a release build.

Choosing the right tool

NeedBest toolWhy
Simple state changes (expand, fade, colour)Reanimated CSS transitionsDeclarative, little code
Looping or keyframe effects (pulse, shimmer)Reanimated CSS animationsKeyframes without manual timing
Gestures, drag, swipe, sheetsReanimated worklets + Gesture HandlerRuns on the UI thread with the finger
Scroll-linked effectsuseAnimatedScrollHandler + interpolateNo JavaScript per scroll event
Designer-made illustrationsLottiePlays After Effects animations as vectors
Charts, heat maps, custom drawingReact Native Skia (with Reanimated)GPU drawing instead of many views
Legacy codeReact Native's Animated with useNativeDriverAcceptable for simple cases; migrate over time

Mixing these is normal. A screen might use CSS transitions for its cards, a gesture-driven bottom sheet, and a Skia chart, each doing the job it's best at.

Rules that keep every pattern smooth

DoAvoid
Animate transform and opacityAnimating width, height, top, left on large views every frame
Keep frame-by-frame values in shared valuessetState on every animation frame
Cross to JavaScript only at the end (with scheduleOnRN from react-native-worklets; runOnJS is deprecated in Reanimated 4)Calling JavaScript functions from every frame of a worklet
Memoise animated components and their childrenRe-rendering a whole screen because an animation started
Test in release builds on mid-range AndroidJudging smoothness in a debug build on a simulator

Accessibility: reduced motion

Some users turn on "Reduce motion" in their system settings. Reanimated's useReducedMotion() hook tells you when, so you can replace large movements with fades or skip decorative animations. Layout animations also support reduced-motion configuration.

FAQ

What is the best animation library for React Native?

Reanimated, for almost everything: gestures, scroll effects, transitions and layout animations, running on the UI thread. Reanimated 4 adds a CSS-style API for simple transitions. Use Lottie for designer-made vector animations and React Native Skia for custom drawing and charts.

Why do my React Native animations lag?

Usually because the animation depends on the JavaScript thread (state updates per frame, the old Animated API without the native driver) or animates layout properties. Move per-frame values into Reanimated shared values, animate transforms and opacity, and test in a release build.

Does Reanimated 4 work with the old architecture?

No. Reanimated 4 supports only the New Architecture. Apps still on the legacy architecture need to stay on Reanimated 3 until they migrate, which is another reason to plan that migration.

How do I make a header animate on scroll in React Native?

Use Reanimated's useAnimatedScrollHandler to store the scroll offset in a shared value, then derive the header's transform or opacity with interpolate inside useAnimatedStyle. Because it all runs on the UI thread, the header stays in sync with the finger.

Next steps

If your app's gestures or animations stutter on real devices, I can profile them and move the work where it belongs, so the motion matches the finger again. Get in touch, or see how I work as a React Native developer.

Sources

Bhuwanesh Sisodia

Bhuwanesh Sisodia

Full-Stack, Mobile & AI Engineer

Bhuwanesh Sisodia is a Full-Stack, Mobile & AI Engineer based in Mumbai, with 7+ years of experience and 50+ apps shipped to the App Store and Google Play. He works independently with startups and product teams worldwide.

About meGitHubLinkedInX

Related