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 1000+ 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
Recommended Reading
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 |
Explore More
- 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
Viprasol in Action
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.
External Resources
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
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.