Back to Blog

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.

Viprasol Tech Team
November 4, 2026
14 min read

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 ApproachFrame Budget UsedJank 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

ScopeTime EstimateCost 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


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.

Talk to our team โ†’ | See our web development services โ†’

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.