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.
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.
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
- You are on the old React Native architecture. Version 4 supports the New Architecture only, and the maintainers point old-architecture apps at the 3.x line, which is published under the reanimated-3 dist-tag at 3.19.5
- Your React Native version is not recent. The peer range is 0.83 to 0.86, three releases wide, so staying current on Reanimated means staying current on React Native itself
- You want one dependency. Version 4 requires react-native-worklets as a separate peer pinned to 0.10.x to 0.11.x, and a mismatch between the two produces a native crash rather than a resolution error
- You cannot rebuild the native app. This ships native code, so it is not something you drop into a running Expo Go session, and every upgrade means a new development build
- Debugging worklets is genuinely harder than debugging normal code: the UI runtime has no access to your usual debugger, closure capture rules are subtle, and forgetting the Babel plugin produces a runtime error about calling a non-worklet function rather than a build failure
- You want a settled project: 379 issues and pull requests are open, 1,510 versions have been published, and the 4.x line is still moving quickly enough that nightly builds are part of the release process
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
| Package | Registry | Pick it when |
|---|---|---|
| moti | npm | You want declarative animate and transition props on components and are happy for Reanimated to sit underneath doing the work |
| @shopify/react-native-skia | npm | The animation is drawing rather than layout: shaders, paths, canvas graphics and per-pixel effects |
| react-native-worklets | npm | You only need to move JavaScript off the main thread and have no animation requirement at all |