Reanimated Animations
High-performance animations in React Native using Reanimated and Gesture Handler
You are an expert in animations for building cross-platform mobile apps with React Native.
## Key Points
- Always mark worklet functions with the `"worklet"` directive or use them inside `useAnimatedStyle` / gesture callbacks where the Reanimated Babel plugin infers it.
- Use `withSpring` for natural-feeling interactions (drags, button presses) and `withTiming` for precise duration-controlled transitions (fade in/out, color changes).
- Use `runOnJS` to call regular JavaScript functions from worklets (e.g., dispatching state updates, navigation). Never call JS functions directly from the UI thread.
- Set `scrollEventThrottle={16}` on ScrollView when using `useAnimatedScrollHandler` for smooth 60fps tracking.
- Prefer layout animations (`entering`, `exiting`, `layout` props) over manual mount/unmount animations — they are declarative and handle interruption correctly.
- Use `useReducedMotion` from Reanimated to respect the user's accessibility settings.
- **Accessing shared values with `.value` in animated styles**: Inside `useAnimatedStyle`, always read `.value`. Outside worklets, `.value` triggers a re-render — this is by design.
- **Gesture conflicts**: When nesting gestures (e.g., swipeable row inside a scrollable list), use `Gesture.Simultaneous`, `Gesture.Exclusive`, or `Gesture.Race` to define precedence.
- **Missing GestureHandlerRootView**: The app root must be wrapped in `<GestureHandlerRootView style={{ flex: 1 }}>`. Expo Router does this automatically.skilldb get react-native-skills/Reanimated AnimationsFull skill: 409 linesReanimated Animations — React Native
You are an expert in animations for building cross-platform mobile apps with React Native.
Core Philosophy
The fundamental rule of React Native animation is: do not animate on the JavaScript thread. The JS thread handles your business logic, state management, and component rendering. If animations run there too, any expensive computation (re-render, API call, JSON parsing) causes animation frames to drop, producing visible jank. Reanimated solves this by compiling animation logic into worklets that execute directly on the native UI thread, completely independent of JavaScript. This architectural split is why Reanimated animations stay smooth at 60fps even when the JS thread is busy.
Gesture-driven animations should feel physically natural. When a user drags a card, the card should follow their finger with zero perceptible delay. When they release it, it should snap into place with a spring animation that mimics real-world physics, not a linear ease that feels robotic. Spring animations (withSpring) handle interruption gracefully because they account for current velocity -- if the user re-grabs a card mid-animation, the spring picks up from the current position and velocity without a jarring reset. Duration-based animations (withTiming) are appropriate for transitions where precise timing matters (fade-ins, color changes) but feel wrong for direct manipulation.
Shared values are the bridge between the UI thread and the React render cycle. A useSharedValue variable lives on the UI thread and can be read and written from worklets without crossing the JS bridge. Reading .value in a useAnimatedStyle worklet is fast -- it happens on the UI thread. Reading .value outside a worklet triggers a synchronous bridge call, which is by design but should not happen in hot paths. Understanding this dual nature is essential for writing performant animation code.
Anti-Patterns
-
Animating properties that cannot run on the UI thread: Only
transform,opacity,backgroundColor,width,height, and a few other layout properties can be animated on the UI thread. Attempting to animatefontSize,fontWeight, orborderStyleeither fails silently or falls back to the JS thread, negating Reanimated's performance benefits. -
Using React state to drive frame-by-frame animations: Calling
setState60 times per second to animate a value causes 60 re-renders per second, overwhelming the JS thread. Shared values anduseAnimatedStylebypass React's render cycle entirely, letting the UI thread handle animation frames independently. -
Nesting gestures without explicit composition: When a swipeable row is inside a scrollable list, both gestures compete for the same touch events. Without
Gesture.Simultaneous,Gesture.Exclusive, orGesture.Raceto define precedence, one gesture randomly wins, creating an unpredictable user experience. -
Missing GestureHandlerRootView at the app root: The Gesture Handler library requires
<GestureHandlerRootView style={{ flex: 1 }}>wrapping the entire app. Without it, gestures silently fail on Android. Expo Router sets this up automatically, but bare React Native projects must add it manually. -
Not handling interrupted animations: When a user triggers a new animation before the previous one finishes, shared values must update correctly from the current position, not from the original start position.
withSpringhandles this naturally by reading current velocity, but customwithTimingsequences may need explicit cancellation logic.
Overview
React Native Reanimated runs animations on the UI thread using worklets — small JavaScript functions compiled to run outside the JS thread. Combined with React Native Gesture Handler, it enables fluid 60fps+ gesture-driven animations. Reanimated 3 introduced a simplified API with useAnimatedStyle, useSharedValue, and layout animations. This skill covers Reanimated 3, Gesture Handler 2, and common animation patterns for mobile UIs.
Core Concepts
Shared Values and Animated Styles
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from "react-native-reanimated";
function ScaleButton() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withSpring(0.95, { damping: 15, stiffness: 150 });
};
const handlePressOut = () => {
scale.value = withSpring(1, { damping: 15, stiffness: 150 });
};
return (
<Animated.View style={animatedStyle}>
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
<Text style={styles.buttonText}>Press Me</Text>
</Pressable>
</Animated.View>
);
}
Animation Types
import {
withSpring,
withTiming,
withDecay,
withRepeat,
withSequence,
withDelay,
Easing,
} from "react-native-reanimated";
// Spring (physics-based)
offset.value = withSpring(100, {
damping: 12,
stiffness: 100,
mass: 0.5,
});
// Timing (duration-based)
opacity.value = withTiming(1, {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
// Decay (momentum-based, for fling gestures)
translationX.value = withDecay({
velocity: velocityX,
clamp: [-200, 200],
});
// Sequence
scale.value = withSequence(
withTiming(1.2, { duration: 150 }),
withTiming(0.9, { duration: 100 }),
withSpring(1)
);
// Repeat
rotation.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1, // Infinite
false // Don't reverse
);
// Delay
opacity.value = withDelay(500, withTiming(1, { duration: 300 }));
Gesture Handler Integration
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from "react-native-reanimated";
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>Drag me</Text>
</Animated.View>
</GestureDetector>
);
}
Implementation Patterns
Swipe-to-Delete
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
interpolate,
Extrapolation,
} from "react-native-reanimated";
const SWIPE_THRESHOLD = -100;
function SwipeableRow({ onDelete, children }: SwipeableRowProps) {
const translateX = useSharedValue(0);
const panGesture = Gesture.Pan()
.activeOffsetX([-10, 10])
.onUpdate((event) => {
translateX.value = Math.min(0, event.translationX);
})
.onEnd((event) => {
if (translateX.value < SWIPE_THRESHOLD) {
translateX.value = withTiming(-200, { duration: 200 });
runOnJS(onDelete)();
} else {
translateX.value = withSpring(0);
}
});
const rowStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const deleteStyle = useAnimatedStyle(() => ({
opacity: interpolate(
translateX.value,
[-100, 0],
[1, 0],
Extrapolation.CLAMP
),
}));
return (
<View>
<Animated.View style={[styles.deleteBackground, deleteStyle]}>
<Text style={styles.deleteText}>Delete</Text>
</Animated.View>
<GestureDetector gesture={panGesture}>
<Animated.View style={rowStyle}>{children}</Animated.View>
</GestureDetector>
</View>
);
}
Layout Animations
import Animated, {
FadeIn,
FadeOut,
SlideInRight,
SlideOutLeft,
LinearTransition,
BounceIn,
} from "react-native-reanimated";
function AnimatedList({ items }: { items: Item[] }) {
return (
<Animated.FlatList
data={items}
itemLayoutAnimation={LinearTransition.springify()}
renderItem={({ item, index }) => (
<Animated.View
entering={FadeIn.delay(index * 50).springify()}
exiting={SlideOutLeft.duration(200)}
layout={LinearTransition.springify()}
style={styles.listItem}
>
<Text>{item.name}</Text>
</Animated.View>
)}
/>
);
}
// Conditional rendering with layout animations
function ExpandableCard() {
const [expanded, setExpanded] = useState(false);
return (
<Pressable onPress={() => setExpanded(!expanded)}>
<Animated.View layout={LinearTransition.springify()} style={styles.card}>
<Text>Header</Text>
{expanded && (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Text>Expanded content goes here</Text>
</Animated.View>
)}
</Animated.View>
</Pressable>
);
}
Shared Element Transitions
import Animated, { SharedTransition } from "react-native-reanimated";
const customTransition = SharedTransition.custom((values) => {
"worklet";
return {
originX: withSpring(values.targetOriginX),
originY: withSpring(values.targetOriginY),
width: withSpring(values.targetWidth),
height: withSpring(values.targetHeight),
};
});
// List screen
function ListScreen() {
return items.map((item) => (
<Pressable onPress={() => navigate("Detail", { id: item.id })}>
<Animated.Image
sharedTransitionTag={`image-${item.id}`}
sharedTransitionStyle={customTransition}
source={{ uri: item.imageUrl }}
style={styles.thumbnail}
/>
</Pressable>
));
}
// Detail screen
function DetailScreen({ route }) {
const { id } = route.params;
return (
<Animated.Image
sharedTransitionTag={`image-${id}`}
sharedTransitionStyle={customTransition}
source={{ uri: getItem(id).imageUrl }}
style={styles.heroImage}
/>
);
}
Interpolation and Scroll Animations
import Animated, {
useAnimatedScrollHandler,
useSharedValue,
useAnimatedStyle,
interpolate,
Extrapolation,
} from "react-native-reanimated";
function ParallaxHeader() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => ({
height: interpolate(
scrollY.value,
[-100, 0, 200],
[400, 300, 100],
Extrapolation.CLAMP
),
opacity: interpolate(
scrollY.value,
[0, 200],
[1, 0],
Extrapolation.CLAMP
),
}));
const titleStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
scrollY.value,
[0, 200],
[0, -50],
Extrapolation.CLAMP
),
},
{
scale: interpolate(
scrollY.value,
[0, 200],
[1, 0.7],
Extrapolation.CLAMP
),
},
],
}));
return (
<View style={{ flex: 1 }}>
<Animated.View style={[styles.header, headerStyle]}>
<Animated.Text style={[styles.title, titleStyle]}>
Profile
</Animated.Text>
</Animated.View>
<Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
<View style={{ paddingTop: 300 }}>{/* Content */}</View>
</Animated.ScrollView>
</View>
);
}
Best Practices
- Always mark worklet functions with the
"worklet"directive or use them insideuseAnimatedStyle/ gesture callbacks where the Reanimated Babel plugin infers it. - Use
withSpringfor natural-feeling interactions (drags, button presses) andwithTimingfor precise duration-controlled transitions (fade in/out, color changes). - Use
runOnJSto call regular JavaScript functions from worklets (e.g., dispatching state updates, navigation). Never call JS functions directly from the UI thread. - Set
scrollEventThrottle={16}on ScrollView when usinguseAnimatedScrollHandlerfor smooth 60fps tracking. - Prefer layout animations (
entering,exiting,layoutprops) over manual mount/unmount animations — they are declarative and handle interruption correctly. - Use
useReducedMotionfrom Reanimated to respect the user's accessibility settings.
Common Pitfalls
- Accessing shared values with
.valuein animated styles: InsideuseAnimatedStyle, always read.value. Outside worklets,.valuetriggers a re-render — this is by design. - Gesture conflicts: When nesting gestures (e.g., swipeable row inside a scrollable list), use
Gesture.Simultaneous,Gesture.Exclusive, orGesture.Raceto define precedence. - Missing GestureHandlerRootView: The app root must be wrapped in
<GestureHandlerRootView style={{ flex: 1 }}>. Expo Router does this automatically. - Animating non-animatable properties: Only
transform,opacity,backgroundColor,width,height, and a few other properties can be animated on the UI thread. AnimatingborderRadiusworks, butfontSizedoes not. - Not handling interrupted animations: When a user triggers a new animation before the previous one finishes, ensure shared values update correctly.
withSpringhandles this well by reading the current velocity.
Install this skill directly: skilldb add react-native-skills
Related Skills
Eas Build Ota Updates
Deploying React Native apps with EAS Build, app store submission, and OTA updates via EAS Update
Expo Managed Workflow
Expo managed workflow for rapid React Native development with minimal native configuration
Native Modules Turbo Modules
Creating native modules and Turbo Modules to bridge platform-specific functionality into React Native
React Navigation Patterns
React Navigation patterns for stack, tab, drawer, and nested navigators in React Native
Offline Storage
Offline storage strategies in React Native using AsyncStorage, MMKV, and WatermelonDB
State Management Zustand Jotai
State management in React Native using Zustand and Jotai for scalable, performant app state