Module 9: Animations and Gestures
React Native Reanimated
Build 60fps animations that run entirely on the UI thread with the modern animation library
π― Learning Objectives
- Understand why Reanimated improves upon the built-in Animated API
- Work with shared values and the worklet concept
- Use useAnimatedStyle for dynamic styling
- Implement animations with useSharedValue and withTiming/withSpring
- Create animated reactions and derived values
Why Reanimated?
React Native Reanimated is a powerful animation library that runs animations directly on the UI thread, eliminating the performance bottlenecks of the JavaScript bridge. While the built-in Animated API is good for basic animations, Reanimated unlocks possibilities for complex, gesture-driven, and highly performant animations.
Animated API vs Reanimated
flowchart LR
subgraph Animated["Built-in Animated API"]
A1[JS Thread] -->|"Bridge (async)"| A2[UI Thread]
A3[Limited native driver support]
A4[Can't animate layout props natively]
end
subgraph Reanimated["Reanimated"]
B1[Worklet Code] -->|"Runs directly"| B2[UI Thread]
B3[Full native execution]
B4[Animate any prop]
end
style Animated fill:#fff3e0
style Reanimated fill:#e8f5e9
Key Advantages
| Feature | Animated API | Reanimated |
|---|---|---|
| Transform animations | β Native driver | β UI thread |
| Layout animations | β JS thread only | β UI thread |
| Color animations | β JS thread only | β UI thread |
| Gesture integration | β οΈ Limited | β Seamless |
| Conditional logic | β Requires JS | β In worklets |
| Layout transitions | β Not supported | β Built-in |
π‘ When to Use Reanimated
- Complex gesture-driven animations (swipe cards, drag-to-delete)
- Animations that respond to scroll position
- Layout animations (width, height, position changes)
- Any animation where 60fps is critical
- Animations with conditional logic or calculations
Installation and Setup
Reanimated requires some configuration, but Expo makes it straightforward.
Installing with Expo
# Install Reanimated
npx expo install react-native-reanimated
# Reanimated is included in Expo SDK 49+ by default
# Just import and use!
Babel Configuration
Update your babel.config.js to include the Reanimated plugin:
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
// Reanimated plugin must be listed last
'react-native-reanimated/plugin',
],
};
};
β οΈ Important
After adding the Babel plugin, you need to clear the Metro bundler cache:
npx expo start --clear
Basic Import Pattern
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withDelay,
withSequence,
withRepeat,
Easing,
interpolate,
Extrapolation,
runOnJS,
} from 'react-native-reanimated';
Animated Styles
The useAnimatedStyle hook creates style objects that automatically update when shared values change, all on the UI thread.
Basic useAnimatedStyle
import React from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function AnimatedBox() {
const scale = useSharedValue(1);
// This function runs on the UI thread
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
const handlePress = () => {
scale.value = withSpring(scale.value === 1 ? 1.5 : 1);
};
return (
<View style={styles.container}>
<Pressable onPress={handlePress}>
<Animated.View style={[styles.box, animatedStyles]} />
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: '#007AFF',
borderRadius: 10,
},
});
Multiple Animated Properties
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
interpolateColor,
} from 'react-native-reanimated';
function MultiPropertyAnimation() {
const progress = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => {
return {
opacity: progress.value,
transform: [
{ translateY: (1 - progress.value) * 50 },
{ scale: 0.8 + (progress.value * 0.2) },
{ rotate: `${progress.value * 360}deg` },
],
backgroundColor: interpolateColor(
progress.value,
[0, 1],
['#FF0000', '#00FF00']
),
};
});
const animate = () => {
progress.value = withTiming(progress.value === 0 ? 1 : 0, {
duration: 500,
});
};
return (
<Pressable onPress={animate}>
<Animated.View style={[styles.box, animatedStyles]} />
</Pressable>
);
}
Conditional Styles in Worklets
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function ConditionalAnimation() {
const isExpanded = useSharedValue(false);
const position = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => {
// You can use conditionals inside useAnimatedStyle!
const backgroundColor = isExpanded.value ? '#34C759' : '#007AFF';
const borderRadius = isExpanded.value ? 20 : 10;
return {
width: isExpanded.value ? 200 : 100,
height: isExpanded.value ? 200 : 100,
backgroundColor,
borderRadius,
transform: [
{ translateX: position.value },
],
};
});
const toggle = () => {
isExpanded.value = !isExpanded.value;
position.value = withSpring(isExpanded.value ? 50 : 0);
};
return (
<Pressable onPress={toggle}>
<Animated.View style={animatedStyles} />
</Pressable>
);
}
β οΈ useAnimatedStyle Rules
- Must return a style object
- Runs on UI thread (it's a worklet)
- Don't call React hooks or access React state inside
- Don't use
console.log(userunOnJSinstead) - Must be used with
Animated.Viewor other Animated components
Animation Functions
Reanimated provides animation functions that define how values change over time. These replace the Animated.timing, Animated.spring, and Animated.decay from the built-in API.
withTiming
Creates a timing-based animation with configurable duration and easing.
import Animated, {
useSharedValue,
withTiming,
Easing,
} from 'react-native-reanimated';
function TimingExample() {
const opacity = useSharedValue(0);
const translateX = useSharedValue(-100);
const fadeIn = () => {
// Basic timing animation
opacity.value = withTiming(1, {
duration: 500,
});
};
const slideIn = () => {
// Timing with custom easing
translateX.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
};
const animateWithCallback = () => {
opacity.value = withTiming(1, { duration: 500 }, (finished) => {
// Callback runs on UI thread
if (finished) {
// Animation completed
console.log('Animation finished!'); // Won't work!
// Use runOnJS for JS thread operations
}
});
};
return (/* ... */);
}
// Available Easing functions
const easingExamples = {
linear: Easing.linear,
ease: Easing.ease,
// Quadratic
easeIn: Easing.in(Easing.quad),
easeOut: Easing.out(Easing.quad),
easeInOut: Easing.inOut(Easing.quad),
// Cubic
cubicIn: Easing.in(Easing.cubic),
cubicOut: Easing.out(Easing.cubic),
// Exponential
expIn: Easing.in(Easing.exp),
expOut: Easing.out(Easing.exp),
// Elastic and bounce
elastic: Easing.elastic(1),
bounce: Easing.bounce,
// Back (overshoots)
back: Easing.back(1.5),
// Custom bezier curve
bezier: Easing.bezier(0.25, 0.1, 0.25, 1),
};
withSpring
Creates physics-based spring animations. These feel more natural for interactive UI.
import Animated, {
useSharedValue,
withSpring,
} from 'react-native-reanimated';
function SpringExample() {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
// Default spring configuration
const bounceDefault = () => {
scale.value = withSpring(1.5);
};
// Custom spring configuration
const bounceCustom = () => {
scale.value = withSpring(1.5, {
damping: 10, // How quickly spring settles (default: 10)
stiffness: 100, // Spring stiffness (default: 100)
mass: 1, // Object mass (default: 1)
overshootClamping: false, // Prevent overshooting (default: false)
restDisplacementThreshold: 0.01, // When to stop (default: 0.01)
restSpeedThreshold: 2, // Speed threshold (default: 2)
});
};
// Very bouncy spring
const bouncySpring = () => {
scale.value = withSpring(1.5, {
damping: 4,
stiffness: 80,
});
};
// Stiff, quick spring
const stiffSpring = () => {
scale.value = withSpring(1.5, {
damping: 20,
stiffness: 200,
});
};
// Spring with initial velocity
const springWithVelocity = () => {
rotation.value = withSpring(360, {
velocity: 1000, // Initial velocity
damping: 15,
});
};
return (/* ... */);
}
// Spring presets for common use cases
const SpringPresets = {
gentle: { damping: 15, stiffness: 100 },
bouncy: { damping: 5, stiffness: 80 },
stiff: { damping: 20, stiffness: 200 },
slow: { damping: 20, stiffness: 50 },
};
withDecay
Creates momentum-based animations that decelerate over time. Perfect for fling gestures.
import Animated, {
useSharedValue,
withDecay,
} from 'react-native-reanimated';
function DecayExample() {
const translateX = useSharedValue(0);
// Basic decay with velocity
const fling = (velocity: number) => {
translateX.value = withDecay({
velocity: velocity, // Initial velocity (required)
deceleration: 0.998, // Deceleration rate (default: 0.998)
});
};
// Decay with boundaries (clamp)
const flingWithBounds = (velocity: number) => {
translateX.value = withDecay({
velocity: velocity,
deceleration: 0.998,
clamp: [-200, 200], // Stop at these boundaries
});
};
// Decay with rubberband effect at boundaries
const flingRubberband = (velocity: number) => {
translateX.value = withDecay({
velocity: velocity,
rubberBandEffect: true,
rubberBandFactor: 0.6,
clamp: [-200, 200],
});
};
return (/* ... */);
}
withDelay
Delays the start of an animation.
import Animated, {
useSharedValue,
withDelay,
withTiming,
withSpring,
} from 'react-native-reanimated';
function DelayExample() {
const opacity = useSharedValue(0);
const translateY = useSharedValue(50);
const animateIn = () => {
// Delay the fade in by 300ms
opacity.value = withDelay(300, withTiming(1, { duration: 500 }));
// Slide up immediately
translateY.value = withSpring(0);
};
return (/* ... */);
}
withSequence
Runs animations one after another.
import Animated, {
useSharedValue,
withSequence,
withTiming,
withSpring,
} from 'react-native-reanimated';
function SequenceExample() {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
// Simple sequence
const pulseAnimation = () => {
scale.value = withSequence(
withTiming(1.2, { duration: 150 }),
withTiming(1, { duration: 150 })
);
};
// Shake animation
const shakeAnimation = () => {
rotation.value = withSequence(
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(-10, { duration: 50 }),
withTiming(10, { duration: 50 }),
withTiming(0, { duration: 50 })
);
};
// Complex sequence
const complexAnimation = () => {
scale.value = withSequence(
withTiming(0.8, { duration: 100 }), // Press down
withSpring(1.2), // Bounce up
withSpring(1) // Settle
);
};
return (/* ... */);
}
withRepeat
Repeats an animation a specified number of times or infinitely.
import Animated, {
useSharedValue,
withRepeat,
withTiming,
withSequence,
Easing,
cancelAnimation,
} from 'react-native-reanimated';
function RepeatExample() {
const rotation = useSharedValue(0);
const pulse = useSharedValue(1);
const bounce = useSharedValue(0);
// Infinite rotation
const startSpinning = () => {
rotation.value = withRepeat(
withTiming(360, {
duration: 1000,
easing: Easing.linear
}),
-1, // -1 = infinite, positive number = that many times
false // reverse: if true, alternates direction
);
};
// Stop spinning
const stopSpinning = () => {
cancelAnimation(rotation);
};
// Pulsing animation (repeats with reverse)
const startPulsing = () => {
pulse.value = withRepeat(
withTiming(1.2, { duration: 500 }),
-1, // Infinite
true // Reverse each iteration
);
};
// Bounce animation (3 times)
const bounceThreeTimes = () => {
bounce.value = withRepeat(
withSequence(
withTiming(-20, { duration: 200 }),
withTiming(0, { duration: 200 })
),
3, // Repeat 3 times
false // Don't reverse
);
};
return (/* ... */);
}
Combining Animations
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withDelay,
withSequence,
withRepeat,
} from 'react-native-reanimated';
function CombinedAnimations() {
const opacity = useSharedValue(0);
const translateY = useSharedValue(50);
const scale = useSharedValue(0.5);
const animateIn = () => {
// Fade in
opacity.value = withTiming(1, { duration: 300 });
// Slide up with spring
translateY.value = withSpring(0, {
damping: 12,
stiffness: 100,
});
// Scale up after a delay, then pulse
scale.value = withDelay(
200,
withSequence(
withSpring(1.1, { damping: 4 }),
withSpring(1, { damping: 8 })
)
);
};
const animateOut = () => {
opacity.value = withTiming(0, { duration: 200 });
translateY.value = withTiming(50, { duration: 200 });
scale.value = withTiming(0.5, { duration: 200 });
};
const animatedStyles = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [
{ translateY: translateY.value },
{ scale: scale.value },
],
}));
return (
<Animated.View style={animatedStyles}>
{/* Content */}
</Animated.View>
);
}
Animation Callbacks
import Animated, {
useSharedValue,
withTiming,
runOnJS,
} from 'react-native-reanimated';
function CallbackExample() {
const [isComplete, setIsComplete] = useState(false);
const opacity = useSharedValue(0);
const animateWithCallback = () => {
opacity.value = withTiming(
1,
{ duration: 500 },
(finished) => {
// This runs on UI thread!
if (finished) {
// To update React state, use runOnJS
runOnJS(setIsComplete)(true);
// Chain another animation
opacity.value = withDelay(
1000,
withTiming(0, { duration: 500 })
);
}
}
);
};
return (/* ... */);
}
Understanding Worklets
Worklets are small JavaScript functions that run on the UI thread. They're the secret behind Reanimated's performanceβby running directly on the UI thread, they avoid the JavaScript bridge entirely.
What is a Worklet?
flowchart LR
subgraph JS["JS Thread"]
A[React Components]
B[State Updates]
C[Event Handlers]
end
subgraph UI["UI Thread"]
D[useAnimatedStyle - worklet]
E[Gesture callbacks - worklet]
F[Animation callbacks - worklet]
end
A -.->|"Babel transform"| D
style JS fill:#fff3e0
style UI fill:#e8f5e9
Implicit Worklets
Some functions are automatically treated as worklets:
import Animated, {
useSharedValue,
useAnimatedStyle,
useDerivedValue,
useAnimatedScrollHandler,
} from 'react-native-reanimated';
// useAnimatedStyle callback is implicitly a worklet
const animatedStyles = useAnimatedStyle(() => {
// This runs on UI thread
return {
opacity: opacity.value,
};
});
// useDerivedValue callback is implicitly a worklet
const derivedOpacity = useDerivedValue(() => {
// This runs on UI thread
return Math.min(opacity.value * 2, 1);
});
// Scroll handler callbacks are implicitly worklets
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
// This runs on UI thread
scrollY.value = event.contentOffset.y;
},
});
Creating Explicit Worklets
import { useSharedValue, runOnUI } from 'react-native-reanimated';
// Define a worklet with the 'worklet' directive
function myWorklet(value: number) {
'worklet';
// This function runs on UI thread
return value * 2;
}
// Use within animated style
const animatedStyles = useAnimatedStyle(() => {
const doubled = myWorklet(opacity.value);
return { opacity: doubled };
});
// Run a worklet from JS thread
function triggerFromJS() {
runOnUI(myWorklet)(5);
}
Worklet Limitations
// β CANNOT do in worklets:
// Access React state
const animatedStyles = useAnimatedStyle(() => {
// return { opacity: reactState }; // ERROR!
});
// Call React hooks
const animatedStyles = useAnimatedStyle(() => {
// const [state, setState] = useState(); // ERROR!
});
// Use console.log directly
const animatedStyles = useAnimatedStyle(() => {
// console.log(opacity.value); // Won't work properly
});
// Access non-shared variables from closure
let regularVariable = 5;
const animatedStyles = useAnimatedStyle(() => {
// return { opacity: regularVariable }; // May not work as expected
});
// β
CAN do in worklets:
// Access shared values
const opacity = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => {
return { opacity: opacity.value }; // β
});
// Use math and calculations
const animatedStyles = useAnimatedStyle(() => {
const calculated = Math.sin(progress.value * Math.PI);
return { opacity: calculated }; // β
});
// Use conditionals
const animatedStyles = useAnimatedStyle(() => {
return {
backgroundColor: isActive.value ? 'green' : 'red', // β
};
});
// Call runOnJS for JS thread operations
const animatedStyles = useAnimatedStyle(() => {
if (progress.value > 0.5) {
runOnJS(myJSFunction)(); // β
}
return { opacity: progress.value };
});
runOnJS - Bridging Back to JS
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
} from 'react-native-reanimated';
function RunOnJSExample() {
const [status, setStatus] = useState('idle');
const progress = useSharedValue(0);
// Regular JS function
const updateStatus = (newStatus: string) => {
setStatus(newStatus);
console.log('Status updated:', newStatus);
};
const startAnimation = () => {
setStatus('animating');
progress.value = withTiming(1, { duration: 1000 }, (finished) => {
// Inside callback, we're on UI thread
if (finished) {
// Use runOnJS to call JS function
runOnJS(updateStatus)('complete');
}
});
};
const animatedStyles = useAnimatedStyle(() => {
// Can also use runOnJS in animated styles
if (progress.value === 1) {
runOnJS(updateStatus)('reached end');
}
return {
opacity: progress.value,
};
});
return (/* ... */);
}
π‘ Worklet Best Practices
- Keep worklets small and focused
- Avoid complex logic that could slow down the UI thread
- Use
runOnJSsparinglyβit crosses the bridge - Don't modify shared values from multiple worklets simultaneously
- Remember: worklets can't access React state or hooks
Derived and Reactive Values
Reanimated provides hooks for creating values that automatically update based on other shared values. This enables reactive programming patterns on the UI thread.
useDerivedValue
Creates a shared value that's computed from other shared values. It automatically updates when its dependencies change.
import Animated, {
useSharedValue,
useDerivedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function DerivedValueExample() {
const x = useSharedValue(0);
const y = useSharedValue(0);
// Derived value: computed from x and y
const distance = useDerivedValue(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2);
});
// Another derived value using the first
const normalizedDistance = useDerivedValue(() => {
const maxDistance = 200;
return Math.min(distance.value / maxDistance, 1);
});
// Use in animated style
const animatedStyles = useAnimatedStyle(() => ({
opacity: 1 - normalizedDistance.value,
transform: [
{ scale: 1 - (normalizedDistance.value * 0.3) },
],
}));
const moveToPosition = (newX: number, newY: number) => {
x.value = withSpring(newX);
y.value = withSpring(newY);
};
return (
<Animated.View style={animatedStyles}>
{/* Content */}
</Animated.View>
);
}
Interpolation with useDerivedValue
import Animated, {
useSharedValue,
useDerivedValue,
useAnimatedStyle,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
function InterpolationExample() {
const scrollY = useSharedValue(0);
// Interpolate scroll position to header height
const headerHeight = useDerivedValue(() => {
return interpolate(
scrollY.value,
[0, 100], // Input range
[200, 80], // Output range
Extrapolation.CLAMP // Don't go outside range
);
});
// Interpolate to header opacity
const headerOpacity = useDerivedValue(() => {
return interpolate(
scrollY.value,
[0, 50, 100],
[1, 0.5, 0],
Extrapolation.CLAMP
);
});
// Interpolate rotation (in degrees)
const rotation = useDerivedValue(() => {
return `${interpolate(
scrollY.value,
[0, 100],
[0, 180]
)}deg`;
});
const headerStyles = useAnimatedStyle(() => ({
height: headerHeight.value,
opacity: headerOpacity.value,
}));
const iconStyles = useAnimatedStyle(() => ({
transform: [{ rotate: rotation.value }],
}));
return (/* ... */);
}
// Extrapolation options:
// Extrapolation.CLAMP - Stop at boundaries
// Extrapolation.EXTEND - Continue beyond boundaries (default)
// Extrapolation.IDENTITY - Return input value outside range
// You can also specify left and right separately:
interpolate(
value,
[0, 100],
[0, 1],
{
extrapolateLeft: Extrapolation.CLAMP,
extrapolateRight: Extrapolation.EXTEND,
}
);
Color Interpolation
import Animated, {
useSharedValue,
useDerivedValue,
useAnimatedStyle,
interpolateColor,
withTiming,
} from 'react-native-reanimated';
function ColorInterpolationExample() {
const progress = useSharedValue(0);
// Simple two-color interpolation
const backgroundColor = useDerivedValue(() => {
return interpolateColor(
progress.value,
[0, 1],
['#FF0000', '#00FF00']
);
});
// Multi-stop color interpolation
const gradientColor = useDerivedValue(() => {
return interpolateColor(
progress.value,
[0, 0.25, 0.5, 0.75, 1],
['#FF0000', '#FF9500', '#FFCC00', '#34C759', '#007AFF']
);
});
const animatedStyles = useAnimatedStyle(() => ({
backgroundColor: backgroundColor.value,
}));
const toggleColor = () => {
progress.value = withTiming(progress.value === 0 ? 1 : 0, {
duration: 500,
});
};
return (
<Pressable onPress={toggleColor}>
<Animated.View style={[styles.box, animatedStyles]} />
</Pressable>
);
}
// Color format options:
// Hex: '#FF0000'
// RGB: 'rgb(255, 0, 0)'
// RGBA: 'rgba(255, 0, 0, 0.5)'
// HSL: 'hsl(0, 100%, 50%)'
// Named: 'red', 'blue', etc.
useAnimatedReaction
Executes a side effect when a shared value changes. Useful for triggering actions based on animation progress.
import Animated, {
useSharedValue,
useAnimatedReaction,
withSpring,
runOnJS,
} from 'react-native-reanimated';
function ReactionExample() {
const [hasReachedEnd, setHasReachedEnd] = useState(false);
const position = useSharedValue(0);
const progress = useSharedValue(0);
// React to position changes
useAnimatedReaction(
// First function: returns the value to track
() => position.value,
// Second function: called when tracked value changes
(currentValue, previousValue) => {
// Both values are available
console.log(`Changed from ${previousValue} to ${currentValue}`);
// Trigger another animation
if (currentValue > 100 && previousValue <= 100) {
progress.value = withSpring(1);
}
}
);
// React to threshold crossing
useAnimatedReaction(
() => progress.value > 0.9,
(reachedEnd, previouslyReached) => {
if (reachedEnd && !previouslyReached) {
// Call JS function when threshold is crossed
runOnJS(setHasReachedEnd)(true);
}
}
);
// React to combined values
useAnimatedReaction(
() => ({
pos: position.value,
prog: progress.value,
}),
(current, previous) => {
// React to changes in either value
if (current.pos !== previous?.pos) {
// Position changed
}
if (current.prog !== previous?.prog) {
// Progress changed
}
}
);
return (/* ... */);
}
Derived Value Patterns
import Animated, {
useSharedValue,
useDerivedValue,
useAnimatedStyle,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
function DerivedPatterns() {
const scrollY = useSharedValue(0);
const isExpanded = useSharedValue(false);
// Clamped value
const clampedScroll = useDerivedValue(() => {
return Math.max(0, Math.min(scrollY.value, 200));
});
// Normalized value (0 to 1)
const normalizedScroll = useDerivedValue(() => {
return clampedScroll.value / 200;
});
// Inverted value
const invertedScroll = useDerivedValue(() => {
return 1 - normalizedScroll.value;
});
// Boolean to number (for animations)
const expandedProgress = useDerivedValue(() => {
return isExpanded.value ? 1 : 0;
});
// Smoothed value (for gesture-driven animations)
const smoothedScroll = useDerivedValue(() => {
return withSpring(scrollY.value, { damping: 20 });
});
// Threshold-based value
const isPastThreshold = useDerivedValue(() => {
return scrollY.value > 100;
});
// Parallax calculations
const parallax = useDerivedValue(() => ({
slow: scrollY.value * 0.3,
medium: scrollY.value * 0.6,
fast: scrollY.value * 1.2,
}));
const backgroundStyles = useAnimatedStyle(() => ({
transform: [{ translateY: parallax.value.slow }],
}));
const midgroundStyles = useAnimatedStyle(() => ({
transform: [{ translateY: parallax.value.medium }],
}));
const foregroundStyles = useAnimatedStyle(() => ({
transform: [{ translateY: parallax.value.fast }],
}));
return (/* ... */);
}
Animated Props
Beyond styles, Reanimated can animate component props directly using useAnimatedProps. This is useful for animating SVG elements, text values, and other non-style properties.
useAnimatedProps
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from 'react-native-reanimated';
import Svg, { Circle } from 'react-native-svg';
// Create animated version of Circle
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
function AnimatedSVGExample() {
const radius = useSharedValue(50);
const strokeWidth = useSharedValue(2);
// Animate SVG props directly
const animatedProps = useAnimatedProps(() => ({
r: radius.value,
strokeWidth: strokeWidth.value,
}));
const expand = () => {
radius.value = withTiming(100, { duration: 500 });
strokeWidth.value = withTiming(5, { duration: 500 });
};
const contract = () => {
radius.value = withTiming(50, { duration: 500 });
strokeWidth.value = withTiming(2, { duration: 500 });
};
return (
<Svg height="250" width="250">
<AnimatedCircle
cx="125"
cy="125"
stroke="#007AFF"
fill="transparent"
animatedProps={animatedProps}
/>
</Svg>
);
}
Animated Text
import Animated, {
useSharedValue,
useAnimatedProps,
useDerivedValue,
withTiming,
} from 'react-native-reanimated';
import { TextInput, StyleSheet } from 'react-native';
// Use TextInput as a display-only text component for animation
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
function AnimatedCounter() {
const count = useSharedValue(0);
// Format the number as text
const text = useDerivedValue(() => {
return `${Math.round(count.value)}`;
});
// Animate the text prop
const animatedProps = useAnimatedProps(() => ({
text: text.value,
defaultValue: text.value,
}));
const increment = () => {
count.value = withTiming(count.value + 100, { duration: 500 });
};
return (
<View>
<AnimatedTextInput
style={styles.counter}
animatedProps={animatedProps}
editable={false}
/>
<Pressable onPress={increment}>
<Text>Add 100</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
counter: {
fontSize: 48,
fontWeight: 'bold',
textAlign: 'center',
},
});
Progress Ring with Animated Props
import React from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
Easing,
} from 'react-native-reanimated';
import Svg, { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface ProgressRingProps {
size?: number;
strokeWidth?: number;
progress: Animated.SharedValue<number>;
}
function ProgressRing({
size = 120,
strokeWidth = 10,
progress
}: ProgressRingProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const animatedProps = useAnimatedProps(() => ({
strokeDashoffset: circumference * (1 - progress.value),
}));
return (
<Svg width={size} height={size}>
{/* Background circle */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#E0E0E0"
strokeWidth={strokeWidth}
fill="transparent"
/>
{/* Animated progress circle */}
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#007AFF"
strokeWidth={strokeWidth}
fill="transparent"
strokeDasharray={circumference}
strokeLinecap="round"
animatedProps={animatedProps}
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</Svg>
);
}
// Usage
function ProgressDemo() {
const progress = useSharedValue(0);
const animate = () => {
progress.value = withTiming(
progress.value === 0 ? 1 : 0,
{ duration: 1000, easing: Easing.inOut(Easing.ease) }
);
};
return (
<View style={styles.container}>
<ProgressRing progress={progress} size={150} />
<Pressable style={styles.button} onPress={animate}>
<Text style={styles.buttonText}>Toggle Progress</Text>
</Pressable>
</View>
);
}
Animated ScrollView Props
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
useAnimatedStyle,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
function AnimatedScrollExample() {
const scrollY = useSharedValue(0);
// Scroll handler (runs on UI thread)
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
onBeginDrag: (event) => {
console.log('Drag started');
},
onEndDrag: (event) => {
console.log('Drag ended');
},
onMomentumBegin: (event) => {
console.log('Momentum started');
},
onMomentumEnd: (event) => {
console.log('Momentum ended');
},
});
// Header animation based on scroll
const headerStyles = useAnimatedStyle(() => {
const height = interpolate(
scrollY.value,
[0, 100],
[200, 80],
Extrapolation.CLAMP
);
const opacity = interpolate(
scrollY.value,
[0, 100],
[1, 0],
Extrapolation.CLAMP
);
return {
height,
opacity,
};
});
return (
<View style={styles.container}>
<Animated.View style={[styles.header, headerStyles]}>
<Text>Collapsing Header</Text>
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
scrollEventThrottle={16}
>
{/* Scroll content */}
</Animated.ScrollView>
</View>
);
}
π‘ Creating Animated Components
Use Animated.createAnimatedComponent() to make any component animatable:
import { Pressable, TextInput } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Animated from 'react-native-reanimated';
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
Hands-On Exercises
Exercise 1: Animated Toggle Switch
Create a custom toggle switch with smooth animations using Reanimated.
Requirements:
- Thumb slides smoothly between positions
- Background color transitions between states
- Spring animation for natural feel
- Track isOn state and call onChange callback
Show Solution
import React, { useState } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
interpolateColor,
runOnJS,
} from 'react-native-reanimated';
interface ToggleSwitchProps {
value: boolean;
onValueChange: (value: boolean) => void;
trackColors?: { on: string; off: string };
thumbColor?: string;
}
function ToggleSwitch({
value,
onValueChange,
trackColors = { on: '#34C759', off: '#E9E9EA' },
thumbColor = '#FFFFFF',
}: ToggleSwitchProps) {
const progress = useSharedValue(value ? 1 : 0);
const handlePress = () => {
const newValue = !value;
progress.value = withSpring(newValue ? 1 : 0, {
damping: 15,
stiffness: 120,
});
onValueChange(newValue);
};
const trackStyles = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(
progress.value,
[0, 1],
[trackColors.off, trackColors.on]
),
}));
const thumbStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: progress.value * 22 },
{ scale: withSpring(progress.value === 0.5 ? 1.1 : 1) },
],
}));
return (
<Pressable onPress={handlePress}>
<Animated.View style={[styles.track, trackStyles]}>
<Animated.View
style={[
styles.thumb,
{ backgroundColor: thumbColor },
thumbStyles,
]}
/>
</Animated.View>
</Pressable>
);
}
// Usage
function ToggleDemo() {
const [isEnabled, setIsEnabled] = useState(false);
return (
<View style={styles.container}>
<ToggleSwitch
value={isEnabled}
onValueChange={setIsEnabled}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
track: {
width: 51,
height: 31,
borderRadius: 16,
padding: 2,
},
thumb: {
width: 27,
height: 27,
borderRadius: 14,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
});
Exercise 2: Collapsing Header
Build a scroll-responsive header that shrinks as the user scrolls down.
Requirements:
- Header shrinks from 200px to 80px as user scrolls
- Title fades out as header collapses
- Background opacity changes with scroll
- Use useAnimatedScrollHandler for scroll tracking
Show Solution
import React from 'react';
import { View, Text, StyleSheet, StatusBar } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedScrollHandler,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const HEADER_MAX_HEIGHT = 200;
const HEADER_MIN_HEIGHT = 80;
const SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
function CollapsingHeader() {
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyles = useAnimatedStyle(() => {
const height = interpolate(
scrollY.value,
[0, SCROLL_DISTANCE],
[HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
Extrapolation.CLAMP
);
return { height };
});
const titleStyles = useAnimatedStyle(() => {
const opacity = interpolate(
scrollY.value,
[0, SCROLL_DISTANCE / 2],
[1, 0],
Extrapolation.CLAMP
);
const translateY = interpolate(
scrollY.value,
[0, SCROLL_DISTANCE],
[0, -20],
Extrapolation.CLAMP
);
const scale = interpolate(
scrollY.value,
[0, SCROLL_DISTANCE],
[1, 0.8],
Extrapolation.CLAMP
);
return {
opacity,
transform: [{ translateY }, { scale }],
};
});
const smallTitleStyles = useAnimatedStyle(() => {
const opacity = interpolate(
scrollY.value,
[SCROLL_DISTANCE / 2, SCROLL_DISTANCE],
[0, 1],
Extrapolation.CLAMP
);
return { opacity };
});
const backgroundStyles = useAnimatedStyle(() => {
const opacity = interpolate(
scrollY.value,
[0, SCROLL_DISTANCE],
[0.3, 1],
Extrapolation.CLAMP
);
return {
backgroundColor: `rgba(0, 122, 255, ${opacity})`,
};
});
// Generate dummy content
const content = Array.from({ length: 30 }, (_, i) => (
<View key={i} style={styles.item}>
<Text style={styles.itemText}>Item {i + 1}</Text>
</View>
));
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<Animated.View style={[styles.header, headerStyles, backgroundStyles]}>
<Animated.Text style={[styles.title, titleStyles]}>
Welcome Back
</Animated.Text>
<Animated.Text style={[styles.smallTitle, smallTitleStyles]}>
Welcome
</Animated.Text>
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: HEADER_MAX_HEIGHT,
paddingBottom: 20,
}}
>
{content}
</Animated.ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 100,
justifyContent: 'flex-end',
paddingBottom: 16,
paddingHorizontal: 20,
},
title: {
color: 'white',
fontSize: 34,
fontWeight: 'bold',
},
smallTitle: {
position: 'absolute',
bottom: 16,
left: 20,
color: 'white',
fontSize: 20,
fontWeight: '600',
},
item: {
backgroundColor: 'white',
marginHorizontal: 16,
marginTop: 12,
padding: 20,
borderRadius: 12,
},
itemText: {
fontSize: 16,
},
});
Exercise 3: Animated Counter
Create a number counter that smoothly animates between values.
Requirements:
- Display animated number that counts up/down
- Format numbers with commas (e.g., 1,234)
- Buttons to increment/decrement by various amounts
- Spring animation for value changes
Show Solution
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet, TextInput } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedProps,
useDerivedValue,
withSpring,
} from 'react-native-reanimated';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
function formatNumber(num: number): string {
return Math.round(num).toLocaleString();
}
function AnimatedCounter() {
const count = useSharedValue(0);
const formattedValue = useDerivedValue(() => {
return formatNumber(count.value);
});
const animatedProps = useAnimatedProps(() => ({
text: formattedValue.value,
defaultValue: formattedValue.value,
}));
const increment = (amount: number) => {
count.value = withSpring(count.value + amount, {
damping: 15,
stiffness: 100,
});
};
const reset = () => {
count.value = withSpring(0, {
damping: 15,
stiffness: 100,
});
};
return (
<View style={styles.container}>
<View style={styles.counterDisplay}>
<AnimatedTextInput
style={styles.counterText}
animatedProps={animatedProps}
editable={false}
/>
</View>
<View style={styles.buttonRow}>
<Pressable
style={[styles.button, styles.decrementButton]}
onPress={() => increment(-100)}
>
<Text style={styles.buttonText}>-100</Text>
</Pressable>
<Pressable
style={[styles.button, styles.decrementButton]}
onPress={() => increment(-10)}
>
<Text style={styles.buttonText}>-10</Text>
</Pressable>
<Pressable
style={[styles.button, styles.decrementButton]}
onPress={() => increment(-1)}
>
<Text style={styles.buttonText}>-1</Text>
</Pressable>
<Pressable
style={[styles.button, styles.incrementButton]}
onPress={() => increment(1)}
>
<Text style={styles.buttonText}>+1</Text>
</Pressable>
<Pressable
style={[styles.button, styles.incrementButton]}
onPress={() => increment(10)}
>
<Text style={styles.buttonText}>+10</Text>
</Pressable>
<Pressable
style={[styles.button, styles.incrementButton]}
onPress={() => increment(100)}
>
<Text style={styles.buttonText}>+100</Text>
</Pressable>
</View>
<Pressable style={styles.resetButton} onPress={reset}>
<Text style={styles.resetText}>Reset</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
counterDisplay: {
backgroundColor: '#f0f0f0',
paddingHorizontal: 40,
paddingVertical: 20,
borderRadius: 16,
marginBottom: 40,
},
counterText: {
fontSize: 48,
fontWeight: 'bold',
textAlign: 'center',
color: '#333',
minWidth: 200,
},
buttonRow: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 10,
},
button: {
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
minWidth: 60,
alignItems: 'center',
},
incrementButton: {
backgroundColor: '#34C759',
},
decrementButton: {
backgroundColor: '#FF3B30',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
resetButton: {
marginTop: 30,
paddingHorizontal: 30,
paddingVertical: 12,
backgroundColor: '#007AFF',
borderRadius: 8,
},
resetText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
Summary
React Native Reanimated revolutionizes animations by running them entirely on the UI thread. This eliminates the performance bottleneck of the JavaScript bridge and enables silky-smooth 60fps animations.
π― Key Takeaways
- Shared Values: Use
useSharedValuefor values that animate without causing re-renders - Animated Styles: Use
useAnimatedStyleto create styles that update on the UI thread - Animation Functions:
withTimingβ Duration-based animations with easingwithSpringβ Physics-based spring animationswithDecayβ Momentum-based decelerationwithDelay,withSequence,withRepeatβ Composition
- Worklets: Functions that run on UI thread; can use math and conditionals but not React state
- runOnJS: Bridge back to JS thread for state updates and console logs
- Derived Values: Use
useDerivedValuefor computed values that auto-update - Animated Props: Use
useAnimatedPropsfor non-style properties like SVG attributes
Reanimated vs Animated API Quick Reference
| Concept | Animated API | Reanimated |
|---|---|---|
| Creating values | useRef(new Animated.Value(0)) |
useSharedValue(0) |
| Timing animation | Animated.timing(value, config).start() |
value.value = withTiming(target, config) |
| Spring animation | Animated.spring(value, config).start() |
value.value = withSpring(target, config) |
| Interpolation | value.interpolate({ inputRange, outputRange }) |
interpolate(value, inputRange, outputRange) |
| Applying styles | style={{ opacity: value }} |
useAnimatedStyle(() => ({ ... })) |
In the next lesson, we'll explore common animation patterns and build practical, reusable animations that you can apply throughout your apps.