MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Native Reanimated: Smooth Mobile Animations

Create performant animations in React Native: worklets, shared values, and layout animations.

React NativeReanimatedAnimationsMobile

By MinhVo

Introduction

Animations are the difference between an app that feels native and one that feels like a web page wrapped in a mobile shell. But creating smooth, 60fps animations in React Native has historically been challenging — the asynchronous bridge between JavaScript and native code introduced latency that made gesture-driven interactions and complex animations feel sluggish. React Native Reanimated solves this by moving animation logic to the native UI thread, executing JavaScript-like code through "worklets" that run synchronously on the UI thread with zero bridge crossings.

Reanimated has become the de facto standard animation library for React Native, used in production by apps with hundreds of millions of users. Its API is designed around shared values that animate on the native thread, gesture-driven animations that respond in real-time, and layout animations that handle mount/unmount transitions gracefully. Combined with React Native Gesture Handler, Reanimated enables the creation of interactions that are indistinguishable from native iOS and Android applications.

Animation Design

Understanding Reanimated: Core Concepts

The Problem with Animated API

React Native's built-in Animated API has a critical limitation: it uses the bridge to communicate animation updates. Each frame, the JavaScript thread calculates the next animation value, serializes it, sends it across the bridge, and the native thread applies it. At 60fps, this means 60 round trips per second through the asynchronous bridge.

When the JavaScript thread is busy (during navigation transitions, list rendering, or data processing), these bridge messages queue up, causing visible animation stutter. The useNativeDriver flag helps for simple animations but doesn't support layout properties like width, height, or flex.

Reanimated's Architecture: Worklets and Shared Values

Reanimated introduces two fundamental concepts that eliminate bridge dependency:

Worklets: Functions marked with 'worklet' directive that are compiled and executed on the UI thread. They look and feel like regular JavaScript but run natively, bypassing the bridge entirely.

'use worklet';
 
// This function runs on the UI thread — zero bridge crossings
const animatedStyle = (progress) => {
  'worklet';
  return {
    opacity: progress.value,
    transform: [{ translateY: progress.value * 50 }],
  };
};

Shared Values: Mutable values that can be read and written from both the JavaScript thread and the UI thread without synchronization overhead. They're the bridge between your React state and your native animations.

const opacity = useSharedValue(0);
 
// Write from JS thread (e.g., in response to API data)
opacity.value = 1;
 
// Read on UI thread (in animated styles)
const animatedStyle = useAnimatedStyle(() => ({
  opacity: opacity.value,
}));

Animation Hooks

Reanimated provides a comprehensive set of animation primitives:

  • useSharedValue() — Creates a mutable shared value
  • useAnimatedStyle() — Derives animated styles from shared values
  • useDerivedValue() — Creates computed values that update automatically
  • useAnimatedGestureHandler() — Handles gestures on the UI thread (deprecated in v3 in favor of Gesture Handler integration)
  • withTiming() — Timing-based animations with easing curves
  • withSpring() — Physics-based spring animations
  • withDecay() — Momentum-based deceleration animations
  • withSequence() — Chains multiple animations
  • withRepeat() — Loops animations

Animation Timeline

Architecture and Design Patterns

Shared Value Lifecycle

Shared values are the backbone of Reanimated's architecture. They use a dual-copy mechanism: one copy lives on the JavaScript thread and one on the UI thread. Updates are synchronized lazily — when the UI thread needs the latest value, it fetches it, minimizing synchronization overhead.

function AnimatedCard() {
  // Shared value: accessible from both JS and UI threads
  const translateX = useSharedValue(0);
  const scale = useSharedValue(1);
 
  // Derived value: computed from shared values on UI thread
  const borderRadius = useDerivedValue(() => {
    return interpolate(scale.value, [1, 1.2], [12, 24], Extrapolation.CLAMP);
  });
 
  // Animated style: applied to component on UI thread
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { scale: scale.value },
    ],
    borderRadius: borderRadius.value,
  }));
 
  return (
    <Animated.View style={[styles.card, animatedStyle]}>
      <Text>Swipe me</Text>
    </Animated.View>
  );
}

Animation Composition

Reanimated's animation functions can be composed to create complex behaviors:

const position = useSharedValue({ x: 0, y: 0 });
 
function animateToPosition(targetX: number, targetY: number) {
  // Run animations in parallel on the UI thread
  position.value = {
    x: withSpring(targetX, { damping: 15, stiffness: 150 }),
    y: withTiming(targetY, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) }),
  };
}
 
function animateSequence() {
  translateX.value = withSequence(
    withTiming(100, { duration: 200 }),
    withTiming(-50, { duration: 150 }),
    withSpring(0, { damping: 8 })
  );
}

Layout Animations

Reanimated v2+ provides declarative layout animations for mount/unmount transitions:

import Animated, {
  FadeInDown,
  FadeOutLeft,
  Layout,
  SlideInRight,
} from 'react-native-reanimated';
 
function TodoList({ items }: { items: Todo[] }) {
  return (
    <FlatList
      data={items}
      renderItem={({ item, index }) => (
        <Animated.View
          entering={FadeInDown.delay(index * 100).springify()}
          exiting={FadeOutLeft.duration(200)}
          layout={Layout.springify()}
          style={styles.todoItem}
        >
          <Text>{item.title}</Text>
        </Animated.View>
      )}
    />
  );
}

Step-by-Step Implementation

Installation

# Install Reanimated
npx expo install react-native-reanimated
 
# Install Gesture Handler (recommended companion)
npx expo install react-native-gesture-handler

Babel Configuration

Add the Reanimated Babel plugin (required for worklet compilation):

// babel.config.js
module.exports = {
  presets: ['babel-preset-expo'],
  plugins: ['react-native-reanimated/plugin'], // Must be last
};

Your First Animated Component

import { StyleSheet, Pressable, Text } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
 
export function AnimatedButton() {
  const scale = useSharedValue(1);
  const opacity = useSharedValue(1);
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    opacity: opacity.value,
  }));
 
  const handlePressIn = () => {
    scale.value = withTiming(0.95, { duration: 100 });
    opacity.value = withTiming(0.8, { duration: 100 });
  };
 
  const handlePressOut = () => {
    scale.value = withSpring(1, { damping: 10, stiffness: 200 });
    opacity.value = withTiming(1, { duration: 150 });
  };
 
  return (
    <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut}>
      <Animated.View style={[styles.button, animatedStyle]}>
        <Text style={styles.buttonText}>Press Me</Text>
      </Animated.View>
    </Pressable>
  );
}
 
const styles = StyleSheet.create({
  button: {
    backgroundColor: '#3b82f6',
    paddingHorizontal: 24,
    paddingVertical: 14,
    borderRadius: 12,
    alignItems: 'center',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Implementing a Swipe-to-Dismiss Gesture

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';
 
export function SwipeToDismiss({
  children,
  onDismiss,
}: {
  children: React.ReactNode;
  onDismiss: () => void;
}) {
  const translateX = useSharedValue(0);
  const opacity = useSharedValue(1);
 
  const gesture = Gesture.Pan()
    .onUpdate((event) => {
      translateX.value = event.translationX;
      opacity.value = 1 - Math.abs(event.translationX) / 300;
    })
    .onEnd((event) => {
      if (Math.abs(event.translationX) > 150) {
        translateX.value = withSpring(event.translationX > 0 ? 400 : -400);
        opacity.value = withTiming(0, { duration: 200 });
        runOnJS(onDismiss)();
      } else {
        translateX.value = withSpring(0);
        opacity.value = withTiming(1, { duration: 200 });
      }
    });
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
    opacity: opacity.value,
  }));
 
  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={animatedStyle}>{children}</Animated.View>
    </GestureDetector>
  );
}

Gesture-Driven Animation

Real-World Use Cases

Use Case 1: Scroll-Linked Parallax Header

import Animated, {
  useAnimatedScrollHandler,
  useAnimatedStyle,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';
 
export function ParallaxScrollView({ children }: { children: React.ReactNode }) {
  const scrollY = useSharedValue(0);
 
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
  });
 
  const headerStyle = useAnimatedStyle(() => ({
    height: interpolate(
      scrollY.value,
      [-100, 0],
      [300, 200],
      Extrapolation.CLAMP
    ),
    opacity: interpolate(
      scrollY.value,
      [0, 150],
      [1, 0],
      Extrapolation.CLAMP
    ),
  }));
 
  const titleStyle = useAnimatedStyle(() => ({
    transform: [{
      translateY: interpolate(
        scrollY.value,
        [0, 200],
        [0, -80],
        Extrapolation.CLAMP
      ),
    }],
    fontSize: interpolate(
      scrollY.value,
      [0, 200],
      [28, 18],
      Extrapolation.CLAMP
    ),
  }));
 
  return (
    <View style={styles.container}>
      <Animated.View style={[styles.header, headerStyle]}>
        <Animated.Text style={[styles.title, titleStyle]}>
          Parallax Header
        </Animated.Text>
      </Animated.View>
      <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
        {children}
      </Animated.ScrollView>
    </View>
  );
}

Use Case 2: Animated Bottom Sheet

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';
 
const SHEET_HEIGHT = 400;
const SNAP_POINTS = { closed: SHEET_HEIGHT, half: SHEET_HEIGHT / 2, open: 0 };
 
export function BottomSheet({ children }: { children: React.ReactNode }) {
  const translateY = useSharedValue(SHEET_HEIGHT);
  const context = useSharedValue(0);
 
  const gesture = Gesture.Pan()
    .onStart(() => {
      context.value = translateY.value;
    })
    .onUpdate((event) => {
      translateY.value = Math.max(
        SNAP_POINTS.open,
        context.value + event.translationY
      );
    })
    .onEnd((event) => {
      const velocity = event.velocityY;
      const position = translateY.value;
 
      if (velocity < -500) {
        translateY.value = withSpring(SNAP_POINTS.open, { damping: 20 });
      } else if (velocity > 500) {
        translateY.value = withSpring(SNAP_POINTS.closed, { damping: 20 });
      } else if (position < SHEET_HEIGHT / 3) {
        translateY.value = withSpring(SNAP_POINTS.open, { damping: 20 });
      } else {
        translateY.value = withSpring(SNAP_POINTS.closed, { damping: 20 });
      }
    });
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));
 
  const backdropStyle = useAnimatedStyle(() => ({
    opacity: interpolate(
      translateY.value,
      [0, SHEET_HEIGHT],
      [0.5, 0],
      Extrapolation.CLAMP
    ),
  }));
 
  return (
    <>
      <Animated.View style={[styles.backdrop, backdropStyle]} />
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.sheet, animatedStyle]}>
          <View style={styles.handle} />
          {children}
        </Animated.View>
      </GestureDetector>
    </>
  );
}

Use Case 3: Skeleton Loading Animation

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withRepeat,
  withTiming,
  withSequence,
  Easing,
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
 
export function SkeletonLoader({ width, height }: { width: number; height: number }) {
  const translateX = useSharedValue(-width);
 
  useEffect(() => {
    translateX.value = withRepeat(
      withSequence(
        withTiming(-width, { duration: 0 }),
        withTiming(width, { duration: 1000, easing: Easing.linear })
      ),
      -1, // Infinite repeat
      false
    );
  }, []);
 
  const shimmerStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));
 
  return (
    <View style={[styles.skeleton, { width, height }]}>
      <Animated.View style={[styles.shimmer, shimmerStyle]}>
        <LinearGradient
          colors={['transparent', 'rgba(255,255,255,0.3)', 'transparent']}
          start={{ x: 0, y: 0 }}
          end={{ x: 1, y: 0 }}
          style={StyleSheet.absoluteFill}
        />
      </Animated.View>
    </View>
  );
}

Best Practices for Production

  1. Always run expensive calculations in worklets: If your animation interpolation involves multiple values, compute it in a worklet rather than on the JS thread to prevent bridge crossings.

  2. Use withSpring for natural-feeling interactions: Spring physics produce animations that feel physical and responsive. Adjust damping (lower = bouncier) and stiffness (higher = faster) to match your design language.

  3. Minimize shared value updates: Each shared value update triggers UI thread work. Batch related updates using runOnJS sparingly and prefer derived values for computed properties.

  4. Use layout animations for list items: FadeIn, SlideInRight, and Layout animations make FlatList items feel polished without manual animation setup.

  5. Prevent memory leaks with cleanup: Cancel ongoing animations in useEffect cleanup by setting shared values to their final state.

  6. Profile with Reanimated's dedicated tools: Use the Reanimated plugin's console warnings to detect worklets that accidentally reference JS-only variables.

  7. Keep worklets pure: Worklets should not access closures over React component state. Only use shared values and constants passed as arguments.

  8. Test on physical devices: Emulators may mask performance issues. Always test animations on real devices, especially low-end Android phones.

Common Pitfalls and Solutions

PitfallImpactSolution
Babel plugin not last in configWorklets fail to compile silentlyEnsure react-native-reanimated/plugin is the last entry in plugins array
Accessing component state inside workletsRuntime error or stale valuesPass values through shared values, not React state references
Forgetting scrollEventThrottle on scroll handlersScroll-linked animations update at low framerateSet scrollEventThrottle={16} for 60fps scroll events
Spring animation never settlingAnimation continues consuming CPUIncrease damping or set restDisplacementThreshold and restSpeedThreshold
Layout animations causing FlatList performance issuesList scrolling becomes jankyUse itemLayoutAnimation prop on FlatList instead of wrapping individual items
Shared value read on wrong threadUndefined behavior or crashesEnsure worklets use value property, not the shared value object itself

Performance Optimization

Measuring Animation Performance

import { configureReanimatedLogger, LogLevel } from 'react-native-reanimated';
 
// Enable strict mode to detect performance issues
configureReanimatedLogger({
  level: LogLevel.warn,
  strict: true, // Warns when worklets access JS-only values
});

Reducing Re-renders

// Bad: Component re-renders on every frame
function BadAnimation() {
  const [offset, setOffset] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => setOffset(prev => prev + 1), 16);
    return () => clearInterval(interval);
  }, []);
  return <View style={{ transform: [{ translateY: offset }] }} />;
}
 
// Good: Animation runs entirely on UI thread
function GoodAnimation() {
  const translateY = useSharedValue(0);
  const style = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));
  
  useEffect(() => {
    translateY.value = withRepeat(withTiming(50, { duration: 1000 }), -1, true);
  }, []);
  
  return <Animated.View style={style} />;
}

Comparison with Alternatives

FeatureReanimatedAnimated APILottieMoti
UI Thread ExecutionYes (worklets)With useNativeDriverYes (pre-rendered)Built on Reanimated
Gesture IntegrationDeep (Gesture Handler)LimitedNoneBuilt on Reanimated
Layout AnimationsBuilt-inNot availableNot availableDeclarative API
Spring PhysicsNative springsBasic springsNot availableDeclarative springs
Learning CurveMedium-HighLowLowLow-Medium
Bundle Size~80KBBuilt-in~150KB~20KB (wraps Reanimated)
Complexity SupportVery highLow-MediumPre-defined onlyMedium

Testing Strategies

import { render } from '@testing-library/react-native';
import Animated from 'react-native-reanimated';
 
// Mock Reanimated for tests
jest.mock('react-native-reanimated', () =>
  require('react-native-reanimated/mock')
);
 
describe('AnimatedButton', () => {
  it('should render without crashing', () => {
    const { getByText } = render(<AnimatedButton />);
    expect(getByText('Press Me')).toBeTruthy();
  });
 
  it('should handle press gesture', async () => {
    const onPress = jest.fn();
    const { getByText } = render(<AnimatedButton onPress={onPress} />);
    
    fireEvent.press(getByText('Press Me'));
    expect(onPress).toHaveBeenCalled();
  });
});

Shared Element Transitions and Layout Animations

Shared element transitions create seamless visual connections between screens by animating elements that appear in both the source and destination screens. Reanimated v3 introduced the SharedTransition API that automatically captures the position and size of tagged elements on both screens and interpolates between them during navigation transitions. Tag elements with sharedTransitionTag props on both screens, and the navigation library handles the animation orchestration automatically.

Layout animations in Reanimated apply automatic enter and exit animations when components mount or unmount from the component tree. The Layout animation modifier specifies how elements animate when their position or size changes due to layout recalculations. Combining entering, exiting, and Layout modifiers on a single component creates fluid animations that respond to data changes without manual animation configuration.

The LinearTransition layout animation interpolates linearly between the old and new layout positions, while SpringTransition uses a spring physics model for more natural motion. For list reordering, combining layout animations with FlatList or FlashList creates smooth reorder effects where items slide into their new positions. The animation duration and spring parameters are configurable to match your design system's motion specifications.

Debugging Reanimated Animations

Debugging worklet-based animations requires specialized tools because worklets execute on a separate UI thread that standard debugging tools cannot inspect. The Reanimated plugin injects console logging into worklets, so console.log calls inside useAnimatedStyle callbacks appear in the Metro console. However, stepping through worklet code with a debugger is not supported, so logging and visual debugging are the primary techniques.

The Reanimated DevTools Chrome extension provides a visual timeline of all running animations, showing the progress, velocity, and current value of each animated property. You can pause, rewind, and scrub through animations frame by frame to identify unexpected jumps or timing issues. The devtools also display the worklet execution time, helping identify performance bottlenecks in complex animation compositions.

For layout animation debugging, enable the layoutAnimationRunner log mode to see the measured positions and sizes of elements before and after layout changes. Common issues include elements measuring incorrectly due to flex layout quirks, animations running in the wrong direction due to coordinate system assumptions, and enter animations firing before the element has received its final dimensions from the layout system.

Future Outlook

Reanimated v3 brought the New Architecture support and improved worklet compilation. The roadmap includes shared element transitions (shared layout animations between screens), web animation support for React Native Web, and deeper integration with the Fabric renderer for even lower-latency gesture responses. The convergence of Reanimated and Gesture Handler into a unified animation system will simplify the API while maintaining the performance characteristics that make it the gold standard for React Native animations.

Reanimated vs LayoutAnimation

React Native's built-in LayoutAnimation API provides simple layout animations but has significant limitations compared to Reanimated. LayoutAnimation runs on the JS thread and can cause dropped frames during complex animations. It also animates all layout changes globally, making it difficult to animate specific elements independently. Reanimated runs animations on the UI thread using native drivers, ensuring smooth 60fps performance even during JS thread blocking operations like state updates and data processing.

Conclusion

React Native Reanimated transforms mobile animations from a performance liability into a competitive advantage. By executing animation logic on the native UI thread through worklets, it eliminates the bridge bottleneck that has historically limited React Native's animation capabilities.

Key takeaways:

  1. Worklets run on the UI thread — zero bridge crossings for animation logic
  2. Shared values are the bridge between JS and UI threads — mutable, synchronized, fast
  3. Spring physics produce natural-feeling interactions — use withSpring as the default
  4. Layout animations are declarative — FadeIn, SlideInRight, Layout for mount/unmount
  5. Gesture Handler integration is seamless — animate in response to gestures without performance cost
  6. Scroll-linked animations are frame-perfect — useAnimatedScrollHandler runs on UI thread
  7. Profile with strict mode — catch JS-thread access in worklets during development

Install Reanimated today with npx expo install react-native-reanimated react-native-gesture-handler and build animations that feel truly native.