React Native Animations: Reanimated 3, Gesture Handler, and Shared Element Transitions
Master React Native animations with Reanimated 3: worklet-based shared values, gesture handler integration, layout animations, and shared element transitions for iOS and Android.
React Native's built-in Animated API runs on the JS thread. Every animation frame, a JS-to-native bridge call moves a value. At 60fps, that's 60 bridge calls per second โ and any JS-thread jank (a heavy re-render, a network callback) drops frames. Reanimated 3 solves this by running animations on the UI thread via worklets: functions that compile to native code and execute without touching JS.
This post covers practical Reanimated 3 patterns: shared values, derived values, gesture integration, layout animations, and shared element transitions between screens.
Setup
# Install Reanimated 3 and Gesture Handler
npx expo install react-native-reanimated react-native-gesture-handler
# For bare React Native
npm install react-native-reanimated react-native-gesture-handler
cd ios && pod install
// babel.config.js โ Reanimated plugin must be last
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// ... other plugins
'react-native-reanimated/plugin', // MUST BE LAST
],
};
// App.tsx โ wrap with GestureHandlerRootView
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* Your navigation/content here */}
</GestureHandlerRootView>
);
}
1. Core Concepts: Shared Values and Worklets
// The fundamental Reanimated 3 pattern
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
withSequence,
withDelay,
runOnJS,
Easing,
} from 'react-native-reanimated';
export function PressableCard() {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
// useAnimatedStyle runs on the UI thread โ never access React state here
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const handlePressIn = () => {
scale.value = withSpring(0.95, { damping: 15, stiffness: 300 });
opacity.value = withTiming(0.8, { duration: 100 });
};
const handlePressOut = () => {
scale.value = withSpring(1, { damping: 15, stiffness: 300 });
opacity.value = withTiming(1, { duration: 150 });
};
return (
<Animated.View style={[styles.card, animatedStyle]}>
<Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
<Text>Press me</Text>
</Pressable>
</Animated.View>
);
}
Derived Values for Computed Animations
import { useDerivedValue, interpolate, Extrapolation } from 'react-native-reanimated';
export function ProgressBar({ progress }: { progress: SharedValue<number> }) {
// useDerivedValue computes on UI thread from another shared value
const width = useDerivedValue(() =>
interpolate(progress.value, [0, 1], [0, 300], Extrapolation.CLAMP)
);
const color = useDerivedValue(() => {
// Interpolate from red โ yellow โ green
if (progress.value < 0.5) {
return `rgb(255, ${Math.round(progress.value * 2 * 255)}, 0)`;
}
return `rgb(${Math.round((1 - progress.value) * 2 * 255)}, 255, 0)`;
});
const barStyle = useAnimatedStyle(() => ({
width: width.value,
backgroundColor: color.value,
}));
return (
<View style={styles.track}>
<Animated.View style={[styles.bar, barStyle]} />
</View>
);
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
2. Gesture Handler Integration
React Native Gesture Handler v2 uses a declarative gesture API that integrates directly with Reanimated shared values.
Draggable Card
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from 'react-native-reanimated';
interface DraggableCardProps {
onDrop: (position: { x: number; y: number }) => void;
}
export function DraggableCard({ onDrop }: DraggableCardProps) {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const startX = useSharedValue(0);
const startY = useSharedValue(0);
const isDragging = useSharedValue(false);
const panGesture = Gesture.Pan()
.onStart(() => {
// Capture starting position
startX.value = translateX.value;
startY.value = translateY.value;
isDragging.value = true;
})
.onUpdate((event) => {
translateX.value = startX.value + event.translationX;
translateY.value = startY.value + event.translationY;
})
.onEnd((event) => {
isDragging.value = false;
// Snap back if not dropped in a valid zone
const finalX = startX.value + event.translationX;
const finalY = startY.value + event.translationY;
// runOnJS bridges back to JS thread for callbacks
runOnJS(onDrop)({ x: finalX, y: finalY });
// Snap back to origin with spring
translateX.value = withSpring(0, { damping: 20, stiffness: 200 });
translateY.value = withSpring(0, { damping: 20, stiffness: 200 });
});
const cardStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: isDragging.value ? withSpring(1.05) : withSpring(1) },
],
zIndex: isDragging.value ? 10 : 1,
shadowOpacity: isDragging.value ? 0.3 : 0.1,
elevation: isDragging.value ? 8 : 2,
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, cardStyle]}>
<Text>Drag me</Text>
</Animated.View>
</GestureDetector>
);
}
Swipeable List Item (Dismiss to Delete)
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
runOnJS,
interpolate,
Extrapolation,
} from 'react-native-reanimated';
const SWIPE_THRESHOLD = -120;
interface SwipeableItemProps {
children: React.ReactNode;
onDelete: () => void;
}
export function SwipeableItem({ children, onDelete }: SwipeableItemProps) {
const translateX = useSharedValue(0);
const itemHeight = useSharedValue<number | null>(null);
const swipeGesture = Gesture.Pan()
.activeOffsetX([-10, 10]) // Only activate for horizontal swipes
.failOffsetY([-5, 5]) // Fail if scrolling vertically
.onUpdate((event) => {
// Only allow left swipe
translateX.value = Math.min(0, event.translationX);
})
.onEnd((event) => {
if (event.translationX < SWIPE_THRESHOLD) {
// Swipe past threshold โ dismiss
translateX.value = withTiming(-500, { duration: 250 }, () => {
if (itemHeight.value !== null) {
itemHeight.value = withTiming(0, { duration: 200 }, () => {
runOnJS(onDelete)();
});
} else {
runOnJS(onDelete)();
}
});
} else {
// Snap back
translateX.value = withSpring(0, { damping: 20 });
}
});
const itemStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
height: itemHeight.value ?? undefined,
overflow: 'hidden',
}));
// Delete button revealed behind the item
const deleteButtonOpacity = useAnimatedStyle(() => ({
opacity: interpolate(
translateX.value,
[0, SWIPE_THRESHOLD],
[0, 1],
Extrapolation.CLAMP
),
}));
return (
<View>
{/* Delete button revealed underneath */}
<Animated.View style={[styles.deleteButton, deleteButtonOpacity]}>
<Text style={styles.deleteText}>Delete</Text>
</Animated.View>
<GestureDetector gesture={swipeGesture}>
<Animated.View style={[styles.item, itemStyle]}>
{children}
</Animated.View>
</GestureDetector>
</View>
);
}
Pinch-to-Zoom
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
export function ZoomableImage({ source }: { source: ImageSourcePropType }) {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onStart(() => {
savedScale.value = scale.value;
})
.onUpdate((event) => {
scale.value = Math.min(Math.max(savedScale.value * event.scale, 0.5), 5);
focalX.value = event.focalX;
focalY.value = event.focalY;
})
.onEnd(() => {
if (scale.value < 1) {
scale.value = withSpring(1);
}
});
const doubleTapGesture = Gesture.Tap()
.numberOfTaps(2)
.onEnd(() => {
scale.value = withSpring(scale.value > 1 ? 1 : 2);
});
// Compose gestures to run simultaneously
const composed = Gesture.Simultaneous(pinchGesture, doubleTapGesture);
const imageStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<GestureDetector gesture={composed}>
<Animated.Image source={source} style={[styles.image, imageStyle]} />
</GestureDetector>
);
}
3. Layout Animations
Layout animations animate components entering, leaving, and repositioning in the layout.
import Animated, { FadeInDown, FadeOutUp, Layout } from 'react-native-reanimated';
interface NotificationItem {
id: string;
message: string;
type: 'success' | 'error' | 'info';
}
export function NotificationList({ items }: { items: NotificationItem[] }) {
return (
<View>
{items.map((item, index) => (
<Animated.View
key={item.id}
entering={FadeInDown.delay(index * 50).springify().damping(15)}
exiting={FadeOutUp.duration(200)}
layout={Layout.springify().damping(15)} // Animate repositioning
style={[styles.notification, styles[item.type]]}
>
<Text>{item.message}</Text>
</Animated.View>
))}
</View>
);
}
// Custom entering animation
import { BaseAnimationBuilder, ComplexAnimationBuilder } from 'react-native-reanimated';
const SlideInFromRight = new ComplexAnimationBuilder()
.duration(300)
.easing(Easing.bezier(0.25, 0.1, 0.25, 1))
.initialValues({ transform: [{ translateX: 400 }], opacity: 0 })
.animations({ transform: [{ translateX: 0 }], opacity: 1 });
Expandable Accordion
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
measure,
useAnimatedRef,
} from 'react-native-reanimated';
export function Accordion({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
const isOpen = useSharedValue(false);
const contentHeight = useSharedValue(0);
const contentRef = useAnimatedRef<Animated.View>();
const toggleAccordion = () => {
if (contentHeight.value === 0) {
// Measure content height on first open
const measured = measure(contentRef);
if (measured) contentHeight.value = measured.height;
}
isOpen.value = !isOpen.value;
};
const contentStyle = useAnimatedStyle(() => ({
height: withTiming(isOpen.value ? contentHeight.value : 0, { duration: 250 }),
opacity: withTiming(isOpen.value ? 1 : 0, { duration: 200 }),
overflow: 'hidden',
}));
const chevronStyle = useAnimatedStyle(() => ({
transform: [
{ rotate: withTiming(isOpen.value ? '180deg' : '0deg', { duration: 250 }) },
],
}));
return (
<View style={styles.accordion}>
<Pressable onPress={toggleAccordion} style={styles.header}>
<Text style={styles.title}>{title}</Text>
<Animated.Text style={chevronStyle}>โผ</Animated.Text>
</Pressable>
<Animated.View style={contentStyle}>
{/* Hidden off-screen to measure height */}
<Animated.View ref={contentRef} style={styles.contentMeasure}>
{children}
</Animated.View>
</Animated.View>
</View>
);
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
4. Shared Element Transitions
Shared element transitions animate a component from one screen to another, giving the illusion that the same element "flies" between screens.
Setup with React Navigation
npx expo install react-native-reanimated react-navigation-shared-element
# or with bare RN:
npm install react-navigation-shared-element react-native-shared-element
// src/navigation/AppNavigator.tsx
import { createSharedElementStackNavigator } from 'react-navigation-shared-element';
const Stack = createSharedElementStackNavigator();
export function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="ProductList" component={ProductListScreen} />
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
sharedElements={(route) => [
// Define which elements are shared between screens
{ id: `product-image-${route.params.productId}` },
{ id: `product-title-${route.params.productId}`, animation: 'fade' },
]}
options={{ headerShown: false }}
/>
</Stack.Navigator>
);
}
// src/screens/ProductListScreen.tsx
import { SharedElement } from 'react-navigation-shared-element';
export function ProductListScreen({ navigation }: Props) {
return (
<FlatList
data={products}
renderItem={({ item }) => (
<Pressable
onPress={() => navigation.navigate('ProductDetail', { productId: item.id })}
>
{/* SharedElement wraps the element that should transition */}
<SharedElement id={`product-image-${item.id}`}>
<Image source={{ uri: item.imageUrl }} style={styles.thumbnail} />
</SharedElement>
<SharedElement id={`product-title-${item.id}`}>
<Text style={styles.productName}>{item.name}</Text>
</SharedElement>
</Pressable>
)}
/>
);
}
// src/screens/ProductDetailScreen.tsx
export function ProductDetailScreen({ route }: Props) {
const { productId } = route.params;
const product = useProduct(productId);
return (
<ScrollView>
{/* Same SharedElement IDs โ Reanimated interpolates between them */}
<SharedElement id={`product-image-${productId}`}>
<Image source={{ uri: product.imageUrl }} style={styles.heroImage} />
</SharedElement>
<SharedElement id={`product-title-${productId}`}>
<Text style={styles.heroTitle}>{product.name}</Text>
</SharedElement>
{/* Rest of detail content */}
</ScrollView>
);
}
Expo Router Shared Transitions (SDK 52+)
// With Expo Router, use the built-in shared transitions API
import { Link } from 'expo-router';
import { SharedTransition } from 'react-native-reanimated';
// In list screen:
<Link href={`/products/${product.id}`} asChild>
<Pressable>
<Animated.Image
source={{ uri: product.imageUrl }}
sharedTransitionTag={`product-image-${product.id}`}
style={styles.thumbnail}
/>
</Pressable>
</Link>
// In detail screen:
<Animated.Image
source={{ uri: product.imageUrl }}
sharedTransitionTag={`product-image-${product.id}`}
style={styles.heroImage}
/>
5. Performance Checklist
// โ
DO: Keep worklets off the JS thread
const animatedStyle = useAnimatedStyle(() => {
// This runs on UI thread โ no React state, no JS APIs
return { opacity: sharedValue.value };
});
// โ DON'T: Access React state in worklets
const animatedStyle = useAnimatedStyle(() => {
return { opacity: someReactState ? 1 : 0 }; // Will throw in production
});
// โ
DO: Use useWorkletCallback for callbacks in worklets
const workletCallback = useWorkletCallback(() => {
'worklet';
return sharedValue.value * 2;
});
// โ
DO: Use runOnJS for React-world side effects
const handleAnimationEnd = useCallback(() => {
setIsVisible(false); // React state update
}, []);
someAnimation.value = withTiming(0, {}, () => {
runOnJS(handleAnimationEnd)(); // Bridge back to JS thread
});
// โ
DO: cancelAnimation before starting a new one
import { cancelAnimation } from 'react-native-reanimated';
const handlePress = () => {
cancelAnimation(scale); // Cancel any in-flight animation
scale.value = withSpring(1.2);
};
Performance Benchmarks
| Animation Approach | Frame Budget Used | Jank on JS Thread Busy |
|---|---|---|
Animated API (JS-driven) | ~30% | Yes โ drops frames |
| Reanimated 2 Worklets | ~5% | No โ UI thread |
| Reanimated 3 Worklets | ~4% | No โ UI thread |
LayoutAnimation (built-in) | ~15% | Partial |
Cost Reference: Animation Engineering
| Scope | Time Estimate | Cost Range |
|---|---|---|
| Basic press/fade animations (5โ10 screens) | 1โ2 days | $800โ1,600 |
| Full swipe gestures (list, drawer, cards) | 3โ5 days | $2,400โ4,000 |
| Shared element transitions (3โ5 flows) | 3โ5 days | $2,400โ4,000 |
| Full animation system (design tokens + all screens) | 2โ4 weeks | $8Kโ20K |
See Also
- React Native Performance Optimization: Hermes, FlatList, and JS Thread
- React Native Testing: Unit, Integration, and Detox E2E
- React Native Offline: AsyncStorage, SQLite, and Conflict Resolution
- React Native Expo EAS: Build Profiles, OTA Updates, and CI/CD
- React Native Navigation Patterns with Expo Router
Working With Viprasol
Building a React Native app that needs animations smooth enough that users don't realize they're animated? Our mobile team designs and implements Reanimated 3 gesture systems, shared element transitions, and layout animations that hold 60fps even under JS-thread load.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.