mrkeyoor.com_
Sat 08 Aug 21:00 UTC
npmMobileupdated 08 Aug 2026

react-native-reanimated

An animation library for React Native that runs animations on the UI thread instead of the JavaScript thread. A Babel plugin turns marked functions into worklets, small pieces of JavaScript that execute in a separate runtime on the UI thread, so animation and gesture code keeps running at full frame rate even when the JS thread is busy rendering or fetching. You hold animated state in shared values, describe styles from those values with useAnimatedStyle, and drive them with withTiming, withSpring and friends. Version 4 also adds a CSS-flavoured API for declarative animations and transitions, and moves the worklet machinery into a separate react-native-worklets package.

Verdict

The default animation layer for React Native, and the only mainstream option when interactions must stay smooth while the JS thread is loaded. The price is real: New Architecture only, a narrow React Native peer range, a second worklets package to keep in step, and a mental model where a whole category of ordinary JavaScript stops working inside your callbacks.

API stability3/5The core surface (shared values, useAnimatedStyle, the with* animations, layout builders) has survived from 2.x through 4.x and is safe to build on. Around it things move: version 4 dropped the old architecture entirely, split worklets into their own package, added the css API, and renamed useScrollViewOffset to useScrollOffset with a deprecation. Each major has needed a migration guide.
Docs5/5The documentation site is unusually good for a React Native library: a full API reference with live interactive examples for nearly every hook, a fundamentals track covering worklets and threading, migration guides per major version, and separate documentation for the worklets package. The gap is that the ecosystem of older tutorials still teaches 2.x and 3.x patterns.
Maintenance5/5The repository was pushed on 2026-08-08 and 4.5.3 was published on 2026-07-22, with nightly builds and a compatibility check workflow running against React Native nightlies. It is funded and staffed by Software Mansion with support from Expo. The 379 open issues reflect the surface area of a native library across two platforms rather than neglect.
Ecosystem5/5Roughly 6,699,791 weekly downloads and 10,949 stars, and it is effectively infrastructure: react-navigation, react-native-gesture-handler, moti, bottom sheet libraries and Expo's own components all build on it. Being pinned by the Expo SDK means most React Native apps already have it installed at a known-good version.

Use it if

  • You need gesture-driven interactions that stay smooth while the JS thread is doing other work, which is the whole reason this library exists
  • You are pairing it with react-native-gesture-handler for drag, swipe, pinch or pull-to-refresh, where both sides run on the UI thread and no bridge hop happens per frame
  • You want entering, exiting and layout transitions on list items and screens without writing measurement code, which the layout animation builders handle
  • You need to run arbitrary JavaScript off the JS thread, since runOnUI, runOnRuntime and createWorkletRuntime are exposed for that directly
Skip it if

Setup reality

Installation is two packages and a Babel change: react-native-reanimated plus react-native-worklets, then react-native-worklets/plugin added as the last entry in the plugins array of babel.config.js. Last is not a style preference, it has to be last, and if it is missing or misplaced you get a runtime error the first time a worklet executes rather than any hint at build time. After that you rebuild the native app and clear the Metro cache, because a stale transform is the second most common cause of the same error. On Expo, do not pick versions yourself: use the ones the SDK pins, since the three-way coupling between React Native, Reanimated and Worklets is tight and the peer range is only three React Native releases wide. Then come the rules of worklets. Code inside useAnimatedStyle or a gesture callback runs in a different runtime, so it can read shared values and captured constants but cannot touch React state, call setState, or invoke most imported functions; crossing back to the JS thread is an explicit runOnJS call. Reading .value on a shared value during render works but does not subscribe the component to changes, which produces animations that appear stuck. Not every style property is animatable on the UI thread, and animating layout properties such as width or flex is far more expensive than transform and opacity. Jest needs the setUpTests helper from the package before any component using it can render. Web support exists but is a separate implementation with its own gaps, so verify anything you rely on there rather than assuming parity.

Patterns

Enable the worklets transformconfigure-babel-plugin

// babel.config.js
module.exports = {
  presets: ['babel-preset-expo'],
  plugins: [
    // every other plugin above this line
    'react-native-worklets/plugin',
  ],
};

It must be the last plugin. Missing or misplaced, the app builds fine and then throws about calling a non-worklet function on the UI thread at the first animation.

Animate a value into a styleanimate-shared-value

import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';

const opacity = useSharedValue(0);

const style = useAnimatedStyle(() => ({
  opacity: opacity.value,
  transform: [{ scale: 0.9 + opacity.value * 0.1 }],
}));

useEffect(() => {
  opacity.value = withTiming(1, { duration: 250 });
}, []);

return <Animated.View style={[styles.card, style]} />;

The style must go on an Animated component, not a plain View. Reading opacity.value in the render body instead of inside useAnimatedStyle gives you a one-time snapshot that never updates.

Use a spring with a preset configspring-animation

import { withSpring, SnappySpringConfig, GentleSpringConfig } from 'react-native-reanimated';

offset.value = withSpring(targetX, SnappySpringConfig);
scale.value = withSpring(1, GentleSpringConfig);

The shipped presets save tuning damping and stiffness by hand. Reanimated3DefaultSpringConfig exists if you are migrating and need the old feel back.

Sequence, repeat and delaycompose-animations

import { withSequence, withRepeat, withDelay, withTiming } from 'react-native-reanimated';

shake.value = withSequence(
  withTiming(-8, { duration: 60 }),
  withRepeat(withTiming(8, { duration: 120 }), 3, true),
  withTiming(0, { duration: 60 })
);

badge.value = withDelay(400, withTiming(1));

withRepeat with -1 runs forever and keeps a frame callback alive, so cancel it on unmount with cancelAnimation to avoid a background animation you cannot see.

Cross back to the JS threadcall-js-from-worklet

import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';

const onDismiss = () => navigation.goBack();

useAnimatedReaction(
  () => translateX.value,
  (current) => {
    if (current > 200) {
      runOnJS(onDismiss)();
    }
  }
);

Any React state update, navigation call or network request from a worklet has to go through runOnJS. Calling it directly throws, because that function was never turned into a worklet.

React to scroll position on the UI threadhandle-scroll

import Animated, { useAnimatedScrollHandler, interpolate, Extrapolation } from 'react-native-reanimated';

const y = useSharedValue(0);
const onScroll = useAnimatedScrollHandler((event) => {
  y.value = event.contentOffset.y;
});

const headerStyle = useAnimatedStyle(() => ({
  height: interpolate(y.value, [0, 120], [180, 64], Extrapolation.CLAMP),
}));

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

Always clamp with Extrapolation.CLAMP unless you want values to run past the input range. Animating height forces layout every frame, so prefer transform when the visual allows it.

Drive an animation from a pan gesturedrive-from-gesture

import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const x = useSharedValue(0);
const pan = Gesture.Pan()
  .onChange((e) => { x.value += e.changeX; })
  .onEnd((e) => { x.value = withDecay({ velocity: e.velocityX }); });

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

return (
  <GestureDetector gesture={pan}>
    <Animated.View style={style} />
  </GestureDetector>
);

Gesture callbacks are worklets too, so the same rules apply. withDecay on release is what makes a drag feel like it has momentum instead of stopping dead.

Animate items entering and leaving a listlayout-animations

import Animated, { FadeInDown, FadeOut, LinearTransition } from 'react-native-reanimated';

{items.map((item) => (
  <Animated.View
    key={item.id}
    entering={FadeInDown.duration(200)}
    exiting={FadeOut}
    layout={LinearTransition.springify()}
  >
    <Row item={item} />
  </Animated.View>
))}

Stable keys are required or React unmounts and remounts rows and every item animates. Wrap a subtree in LayoutAnimationConfig with skipEntering to suppress the animation on first mount.

Measure a node and scroll to itmeasure-and-scroll

import Animated, { useAnimatedRef, measure, scrollTo, runOnUI } from 'react-native-reanimated';

const scrollRef = useAnimatedRef();
const targetRef = useAnimatedRef();

const jump = () => runOnUI(() => {
  'worklet';
  const layout = measure(targetRef);
  if (layout) scrollTo(scrollRef, 0, layout.pageY, true);
})();

measure returns null if the node is not laid out yet, so the null check is not optional. Both measure and scrollTo only work on the UI thread with an animated ref.

Declare an animation the CSS waycss-style-animation

import Animated, { css } from 'react-native-reanimated';

const styles = css.create({
  pulse: {
    animationName: css.keyframes({
      '0%': { opacity: 0.4 },
      '100%': { opacity: 1 },
    }),
    animationDuration: '900ms',
    animationIterationCount: 'infinite',
    animationDirection: 'alternate',
  },
});

return <Animated.View style={styles.pulse} />;

New in version 4 and a good fit for looping decoration where no shared value is needed. It does not replace useAnimatedStyle for anything driven by a gesture.

Honour the system accessibility settingrespect-reduced-motion

import { useReducedMotion, ReducedMotion, withTiming } from 'react-native-reanimated';

const reduced = useReducedMotion();

opacity.value = withTiming(1, {
  duration: reduced ? 0 : 300,
  reduceMotion: ReducedMotion.System,
});

Users who turn on Reduce Motion mean it, and heavy parallax without this check is a common accessibility complaint. ReducedMotionConfig can set the policy for a whole subtree.

Make components render in teststest-with-jest

// jest.setup.js
require('react-native-reanimated').setUpTests();

// in a test
import { advanceAnimationByTime, getAnimatedStyle } from 'react-native-reanimated';

advanceAnimationByTime(300);
expect(getAnimatedStyle(node)).toMatchObject({ opacity: 1 });

Without setUpTests, any component using an animated style throws during render. advanceAnimationByTime is what lets you assert on a finished state instead of waiting.

Alternatives

PackageRegistryPick it when
motinpmYou want declarative animate and transition props on components and are happy for Reanimated to sit underneath doing the work
@shopify/react-native-skianpmThe animation is drawing rather than layout: shaders, paths, canvas graphics and per-pixel effects
react-native-workletsnpmYou only need to move JavaScript off the main thread and have no animation requirement at all