
Introduction
Smooth animations are essential for modern mobile apps—they improve usability, provide feedback, and create a polished user experience. While React Native’s built-in Animated API works for simple cases, complex interactions with gestures require more control and better performance. Reanimated 3 solves these challenges by running animations directly on the UI thread using worklets, enabling buttery-smooth 60 FPS animations even during heavy JavaScript operations.
In this comprehensive guide, you’ll learn how Reanimated 3 works under the hood, how to create high-performance animations with shared values and animated styles, gesture-driven interactions with React Native Gesture Handler, and production-ready patterns for building fluid mobile experiences.
Why Use Reanimated 3 in React Native
The standard React Native Animated API runs on the JavaScript thread, meaning heavy logic, network requests, or complex calculations can cause dropped frames and janky animations. Reanimated fundamentally changes this architecture.
- UI thread execution: Animations run independently of JavaScript.
- 60 FPS guaranteed: Smooth performance regardless of JS workload.
- Native gesture integration: Seamless coordination with Gesture Handler.
- Declarative API: Clean, composable animation definitions.
- Full TypeScript support: Type-safe animations and worklets.
Installation and Setup
# Install Reanimated and Gesture Handler
npm install react-native-reanimated react-native-gesture-handler
# For iOS
cd ios && pod install && cd ..
# For Expo
npx expo install react-native-reanimated react-native-gesture-handler
// babel.config.js - REQUIRED for worklets
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// Reanimated plugin must be listed LAST
'react-native-reanimated/plugin',
],
};
// App.tsx - Setup Gesture Handler at root
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
{/* Your app content */}
);
}
Core Concepts
Shared Values
Shared values are the foundation of Reanimated. They store animated state on the UI thread, allowing animations to run without crossing the JS bridge.
// Basic shared values
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
} from 'react-native-reanimated';
function FadeInBox() {
// Shared value lives on UI thread
const opacity = useSharedValue(0);
const scale = useSharedValue(0.5);
// Animated style reacts to shared value changes
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ scale: scale.value }],
}));
const handlePress = () => {
// Animate with timing (linear interpolation)
opacity.value = withTiming(1, { duration: 300 });
// Animate with spring (physics-based)
scale.value = withSpring(1);
};
return (
);
}
Worklets
Worklets are JavaScript functions that run on the UI thread. They’re marked with the 'worklet' directive.
import { runOnJS, runOnUI } from 'react-native-reanimated';
// Function that runs on UI thread
function calculatePosition(x: number, y: number) {
'worklet';
return Math.sqrt(x * x + y * y);
}
// Calling JS functions from UI thread
function AnimatedComponent() {
const [status, setStatus] = useState('');
const animatedStyle = useAnimatedStyle(() => {
// This runs on UI thread
const distance = calculatePosition(100, 100);
// To call React setState, use runOnJS
if (distance > 50) {
runOnJS(setStatus)('Far from origin');
}
return {
transform: [{ translateX: distance }],
};
});
return ;
}
Animation Functions
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withDelay,
withSequence,
withRepeat,
Easing,
cancelAnimation,
} from 'react-native-reanimated';
function AnimationShowcase() {
const translateX = useSharedValue(0);
const rotation = useSharedValue(0);
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
// 1. Timing animation with easing
const animateWithTiming = () => {
translateX.value = withTiming(200, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // Ease out
});
};
// 2. Spring animation with physics config
const animateWithSpring = () => {
scale.value = withSpring(1.5, {
damping: 10, // Bounciness (lower = more bounce)
stiffness: 100, // Speed of animation
mass: 1, // Weight of object
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
});
};
// 3. Delayed animation
const animateWithDelay = () => {
opacity.value = withDelay(
500, // Wait 500ms
withTiming(0, { duration: 300 })
);
};
// 4. Sequence of animations
const animateSequence = () => {
translateX.value = withSequence(
withTiming(100, { duration: 200 }),
withTiming(-100, { duration: 200 }),
withTiming(0, { duration: 200 })
);
};
// 5. Repeating animation
const animateRepeat = () => {
rotation.value = withRepeat(
withTiming(360, { duration: 1000 }),
-1, // -1 for infinite, or number of repetitions
false // Don't reverse
);
};
// 6. Cancel animation
const stopAnimation = () => {
cancelAnimation(rotation);
cancelAnimation(translateX);
};
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [
{ translateX: translateX.value },
{ rotate: `${rotation.value}deg` },
{ scale: scale.value },
],
}));
return (
);
}
Gesture Handler Integration
// Draggable card with snap back
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 scale = useSharedValue(1);
const rotation = useSharedValue(0);
// Store starting position for continuous drag
const startX = useSharedValue(0);
const startY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onStart(() => {
// Store current position
startX.value = translateX.value;
startY.value = translateY.value;
// Scale up when grabbed
scale.value = withSpring(1.1);
})
.onUpdate((event) => {
// Update position based on gesture
translateX.value = startX.value + event.translationX;
translateY.value = startY.value + event.translationY;
// Rotate based on horizontal velocity
rotation.value = event.velocityX / 50;
})
.onEnd((event) => {
// Snap back to center with spring
translateX.value = withSpring(0);
translateY.value = withSpring(0);
scale.value = withSpring(1);
rotation.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
{ rotate: `${rotation.value}deg` },
],
}));
return (
Drag me!
);
}
const styles = StyleSheet.create({
card: {
width: 200,
height: 300,
backgroundColor: '#fff',
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 20,
elevation: 10,
},
});
Swipeable List Item
// Swipe to delete with actions
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
runOnJS,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const SWIPE_THRESHOLD = 100;
const DELETE_THRESHOLD = 200;
interface SwipeableItemProps {
onDelete: () => void;
children: React.ReactNode;
}
function SwipeableItem({ onDelete, children }: SwipeableItemProps) {
const translateX = useSharedValue(0);
const itemHeight = useSharedValue(80);
const opacity = useSharedValue(1);
const panGesture = Gesture.Pan()
.activeOffsetX([-10, 10])
.onUpdate((event) => {
// Only allow left swipe
translateX.value = Math.min(0, event.translationX);
})
.onEnd((event) => {
const shouldDelete = translateX.value < -DELETE_THRESHOLD;
if (shouldDelete) {
// Animate out and delete
translateX.value = withTiming(-500, { duration: 200 });
itemHeight.value = withTiming(0, { duration: 200 });
opacity.value = withTiming(0, { duration: 200 }, () => {
runOnJS(onDelete)();
});
} else if (translateX.value < -SWIPE_THRESHOLD) {
// Snap to show actions
translateX.value = withSpring(-100);
} else {
// Snap back
translateX.value = withSpring(0);
}
});
const itemStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const containerStyle = useAnimatedStyle(() => ({
height: itemHeight.value,
opacity: opacity.value,
}));
const actionStyle = useAnimatedStyle(() => {
const actionOpacity = interpolate(
translateX.value,
[-100, -50],
[1, 0],
Extrapolation.CLAMP
);
return {
opacity: actionOpacity,
};
});
return (
{/* Delete action behind */}
Delete
{/* Swipeable content */}
{children}
);
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
item: {
backgroundColor: '#fff',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
deleteAction: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 100,
backgroundColor: '#ff3b30',
justifyContent: 'center',
alignItems: 'center',
},
deleteText: {
color: '#fff',
fontWeight: '600',
},
});
Layout Animations
// Entering and exiting animations
import Animated, {
FadeIn,
FadeOut,
SlideInLeft,
SlideOutRight,
ZoomIn,
ZoomOut,
Layout,
BounceIn,
FlipInXUp,
} from 'react-native-reanimated';
function AnimatedList() {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
const addItem = () => {
setItems([...items, `Item ${items.length + 1}`]);
};
const removeItem = (index: number) => {
setItems(items.filter((_, i) => i !== index));
};
return (
{items.map((item, index) => (
{item}
removeItem(index)}>
Remove
))}
);
}
// Custom entering animation
const CustomEntering = () => {
'worklet';
const animations = {
opacity: withTiming(1, { duration: 500 }),
transform: [
{ translateY: withSpring(0) },
{ scale: withSpring(1) },
],
};
const initialValues = {
opacity: 0,
transform: [
{ translateY: -50 },
{ scale: 0.5 },
],
};
return {
initialValues,
animations,
};
};
function CustomAnimatedComponent() {
return (
Custom animation!
);
}
Scroll-Based Animations
// Parallax header with scroll
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedScrollHandler,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const HEADER_HEIGHT = 300;
const STICKY_HEADER_HEIGHT = 100;
function ParallaxHeader() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => {
const translateY = interpolate(
scrollY.value,
[0, HEADER_HEIGHT],
[0, -HEADER_HEIGHT / 2],
Extrapolation.CLAMP
);
const scale = interpolate(
scrollY.value,
[-100, 0],
[1.5, 1],
Extrapolation.CLAMP
);
return {
transform: [{ translateY }, { scale }],
};
});
const titleStyle = useAnimatedStyle(() => {
const opacity = interpolate(
scrollY.value,
[0, HEADER_HEIGHT - STICKY_HEADER_HEIGHT],
[1, 0],
Extrapolation.CLAMP
);
return {
opacity,
};
});
const stickyHeaderStyle = useAnimatedStyle(() => {
const opacity = interpolate(
scrollY.value,
[HEADER_HEIGHT - STICKY_HEADER_HEIGHT - 50, HEADER_HEIGHT - STICKY_HEADER_HEIGHT],
[0, 1],
Extrapolation.CLAMP
);
return {
opacity,
};
});
return (
{/* Parallax image */}
{/* Large title that fades out */}
Welcome
{/* Sticky header that fades in */}
Welcome
{/* Scrollable content */}
{Array.from({ length: 50 }).map((_, i) => (
Item {i + 1}
))}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
headerImage: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: HEADER_HEIGHT,
},
largeTitle: {
position: 'absolute',
top: HEADER_HEIGHT - 80,
left: 20,
zIndex: 1,
},
largeTitleText: {
fontSize: 34,
fontWeight: 'bold',
color: '#fff',
},
stickyHeader: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: STICKY_HEADER_HEIGHT,
backgroundColor: '#fff',
justifyContent: 'flex-end',
paddingBottom: 10,
paddingHorizontal: 20,
zIndex: 2,
},
stickyTitle: {
fontSize: 17,
fontWeight: '600',
},
listItem: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#eee',
backgroundColor: '#fff',
},
});
Animated Reactions
// React to shared value changes
import Animated, {
useSharedValue,
useAnimatedReaction,
withSpring,
runOnJS,
} from 'react-native-reanimated';
function ReactionExample() {
const position = useSharedValue(0);
const [status, setStatus] = useState('idle');
// React when position crosses threshold
useAnimatedReaction(
() => position.value,
(currentValue, previousValue) => {
if (previousValue !== null) {
if (currentValue > 100 && previousValue <= 100) {
runOnJS(setStatus)('past threshold');
} else if (currentValue <= 100 && previousValue > 100) {
runOnJS(setStatus)('below threshold');
}
}
},
[]
);
return (
Status: {status}
);
}
// Derived values
import { useDerivedValue } from 'react-native-reanimated';
function DerivedExample() {
const x = useSharedValue(0);
const y = useSharedValue(0);
// Derived value updates when dependencies change
const distance = useDerivedValue(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2);
});
const animatedStyle = useAnimatedStyle(() => ({
// Use derived value
opacity: interpolate(distance.value, [0, 200], [1, 0]),
}));
return ;
}
Common Mistakes to Avoid
1. Using React State for Animation Values
// WRONG - Causes re-renders and janky animation
const [position, setPosition] = useState(0);
const panGesture = Gesture.Pan()
.onUpdate((e) => {
setPosition(e.translationX); // Triggers re-render!
});
// CORRECT - Use shared values
const position = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((e) => {
position.value = e.translationX; // Updates on UI thread
});
2. Forgetting the Babel Plugin
// babel.config.js - Plugin MUST be last
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// Other plugins...
'react-native-reanimated/plugin', // MUST BE LAST
],
};
3. Accessing .value Outside Worklets
// WRONG - Accessing .value in render
function BadExample() {
const opacity = useSharedValue(1);
return (
{/* Won't animate! */}
Content
);
}
// CORRECT - Use useAnimatedStyle
function GoodExample() {
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
Content
);
}
4. Overusing runOnJS
// WRONG - Calling runOnJS on every frame
const panGesture = Gesture.Pan()
.onUpdate((e) => {
position.value = e.translationX;
runOnJS(setDebugInfo)(e.translationX); // Called 60 times/sec!
});
// CORRECT - Only call runOnJS when necessary
const panGesture = Gesture.Pan()
.onUpdate((e) => {
position.value = e.translationX;
})
.onEnd((e) => {
runOnJS(setDebugInfo)(e.translationX); // Only once at end
});
Best Practices Summary
- Use shared values: Never use React state for animated values.
- Keep worklets small: Minimize computation in animated callbacks.
- Prefer withSpring: Spring animations feel more natural than timing.
- Use interpolate: Map value ranges instead of complex calculations.
- Cancel animations: Clean up running animations when components unmount.
- Test on devices: Animation performance differs from simulators.
- Use layout animations: FadeIn/FadeOut for entering/exiting components.
- Combine gestures: Use Gesture.Simultaneous for multi-touch interactions.
Conclusion
Reanimated 3 provides a powerful and performant way to build animations in React Native. By running animations on the UI thread and integrating deeply with gesture handling, it enables smooth 60 FPS interactions even in complex apps. The declarative API with shared values, worklets, and animated styles makes animation code clean and maintainable.
If you’re deciding on React Native tooling, read Expo vs React Native CLI: Deciding Which to Use. For accessibility considerations, see Accessibility Best Practices for React Native Applications. For official documentation, explore the Reanimated documentation and the React Native Gesture Handler docs.
4 Comments