Skip to main content

Guide

React Native Reanimated vs Moti vs React Native 2026

React Native Reanimated v3 vs Moti vs React Native Skia compared for animations in React Native. Worklets, shared values, Framer-like API, GPU-accelerated.

·PkgPulse Team·
0

React Native Reanimated vs Moti vs React Native Skia: Animation 2026

TL;DR

Smooth animations are the difference between a native-feeling React Native app and a janky one. React Native Reanimated v3 is the foundation — it moves animation calculations to the UI thread via worklets, giving you 60-120fps animations even when the JS thread is busy. Moti is a developer-friendly wrapper on top of Reanimated — a Framer Motion-like declarative API that handles transitions with from/animate props, making common animations 10x less code. React Native Skia operates at a different level — it's a 2D graphics engine backed by Google's Skia renderer, enabling custom drawing, shaders, image filters, and animations that CSS transforms cannot achieve. For UI transitions and micro-interactions: Moti (built on Reanimated). For custom graphics, blurs, and canvas drawing: React Native Skia. For gesture-driven, complex animation sequences: Reanimated directly.

Key Takeaways

  • Reanimated v3 worklets run on UI thread — no JS bridge for animation updates = 60fps guarantee
  • Moti API is Framer Motion-like<MotiView from={{ opacity: 0 }} animate={{ opacity: 1 }} />
  • React Native Skia is GPU-accelerated — renders via Skia's Metal (iOS) and Vulkan (Android)
  • Reanimated GitHub stars: ~9k — the standard animation foundation for RN
  • Moti adds ~30KB to bundle — thin wrapper, no significant overhead on top of Reanimated
  • Skia supports shaders — custom GLSL-like shader effects impossible with standard RN
  • All three work with Expo — Reanimated is included in Expo SDK by default

The React Native Animation Stack

App Layer:
  Moti              ← Declarative, Framer-like (recommended for most UI)
    └── Reanimated  ← Foundation, imperative, gesture-driven (complex sequences)

Graphics Layer:
  React Native Skia ← Canvas, shaders, image filters (custom 2D rendering)

React Native's default Animated API runs on the JS thread — susceptible to dropped frames when your JS thread is busy. Reanimated/Moti/Skia all run on the UI thread.


React Native Reanimated: The Animation Foundation

Reanimated v3 uses "worklets" — small JavaScript functions that compile to native bytecode and run on the UI thread, bypassing the JS bridge entirely.

Installation

npx expo install react-native-reanimated
# Add plugin to babel.config.js:
# plugins: ['react-native-reanimated/plugin']

Shared Values and Animations

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSpring,
  withRepeat,
  withSequence,
  Easing,
  interpolate,
  Extrapolate,
} from "react-native-reanimated";

function FadeInCard() {
  const opacity = useSharedValue(0);
  const scale = useSharedValue(0.9);

  // Animate when component mounts
  useEffect(() => {
    opacity.value = withTiming(1, { duration: 300 });
    scale.value = withSpring(1, { damping: 15, stiffness: 300 });
  }, []);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ scale: scale.value }],
  }));

  return (
    <Animated.View style={[styles.card, animatedStyle]}>
      <Text>Hello!</Text>
    </Animated.View>
  );
}

Gesture-Driven Animation (with Gesture Handler)

import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  runOnJS,
} from "react-native-reanimated";

function SwipeableCard({ onDismiss }: { onDismiss: () => void }) {
  const translateX = useSharedValue(0);
  const rotate = useSharedValue(0);

  const pan = Gesture.Pan()
    .onUpdate((event) => {
      translateX.value = event.translationX;
      rotate.value = (event.translationX / 200) * 15;  // Max 15 degrees
    })
    .onEnd((event) => {
      if (Math.abs(event.translationX) > 120) {
        // Swipe out
        const direction = event.translationX > 0 ? 1 : -1;
        translateX.value = withTiming(direction * 500, { duration: 300 }, () => {
          runOnJS(onDismiss)();  // Call JS function from UI thread
        });
      } else {
        // Snap back
        translateX.value = withSpring(0);
        rotate.value = withSpring(0);
      }
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { rotate: `${rotate.value}deg` },
    ],
  }));

  return (
    <GestureDetector gesture={pan}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Swipe me!</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Complex Sequences and Interpolation

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withRepeat,
  withSequence,
  withTiming,
  interpolate,
  Extrapolate,
} from "react-native-reanimated";

function PulsingDot() {
  const scale = useSharedValue(1);

  useEffect(() => {
    scale.value = withRepeat(
      withSequence(
        withTiming(1.3, { duration: 600 }),
        withTiming(1, { duration: 600 })
      ),
      -1,  // Infinite repeats
      false  // Don't reverse
    );
  }, []);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    // Interpolate: map scale 1→1.3 to opacity 0.6→1
    opacity: interpolate(scale.value, [1, 1.3], [0.6, 1], Extrapolate.CLAMP),
  }));

  return <Animated.View style={[styles.dot, animatedStyle]} />;
}

Moti: Framer Motion for React Native

Moti is a declarative animation library built on Reanimated. Instead of imperatively setting shared values, you declare from and animate states.

Installation

npx expo install moti
# Moti requires Reanimated (install it first)

Declarative Animations

import { MotiView, MotiText, MotiImage } from "moti";

// Simple fade in on mount
function FadeInCard() {
  return (
    <MotiView
      from={{ opacity: 0, translateY: 10 }}
      animate={{ opacity: 1, translateY: 0 }}
      transition={{ type: "timing", duration: 350 }}
    >
      <Text>Hello!</Text>
    </MotiView>
  );
}

// Toggle animation based on state
function Toggle() {
  const [active, setActive] = useState(false);

  return (
    <Pressable onPress={() => setActive(!active)}>
      <MotiView
        animate={{
          backgroundColor: active ? "#5469d4" : "#e5e7eb",
          scale: active ? 1.05 : 1,
        }}
        transition={{ type: "spring", damping: 15 }}
        style={styles.button}
      />
    </Pressable>
  );
}

Skeleton Loading with Moti

import { MotiView } from "moti";
import { Skeleton } from "moti/skeleton";

// Moti's Skeleton component — shimmer effect for loading states
function UserCardSkeleton() {
  return (
    <View style={styles.card}>
      <Skeleton colorMode="light" radius="round" height={50} width={50} />
      <View style={{ marginLeft: 10 }}>
        <Skeleton colorMode="light" height={16} width={120} />
        <View style={{ height: 8 }} />
        <Skeleton colorMode="light" height={12} width={80} />
      </View>
    </View>
  );
}

Staggered List Animation

import { MotiView } from "moti";

function AnimatedList({ items }: { items: string[] }) {
  return (
    <View>
      {items.map((item, index) => (
        <MotiView
          key={item}
          from={{ opacity: 0, translateX: -20 }}
          animate={{ opacity: 1, translateX: 0 }}
          transition={{
            type: "timing",
            duration: 300,
            delay: index * 80,  // Stagger by 80ms per item
          }}
        >
          <Text style={styles.item}>{item}</Text>
        </MotiView>
      ))}
    </View>
  );
}

Presence Animations (Mount/Unmount)

import { AnimatePresence, MotiView } from "moti";

function Modal({ visible, children }: { visible: boolean; children: React.ReactNode }) {
  return (
    <AnimatePresence>
      {visible && (
        <MotiView
          key="modal"
          from={{ opacity: 0, scale: 0.9 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.9 }}  // Animate out on unmount
          transition={{ type: "spring", damping: 20 }}
          style={styles.modal}
        >
          {children}
        </MotiView>
      )}
    </AnimatePresence>
  );
}

React Native Skia: GPU-Accelerated Canvas

React Native Skia brings the Skia 2D graphics engine to React Native — the same engine powering Chrome, Flutter, and Android. Use it for custom 2D graphics, blur effects, image filters, and effects impossible with standard RN transforms.

Installation

npx expo install @shopify/react-native-skia

Basic Canvas Drawing

import {
  Canvas,
  Circle,
  Path,
  Skia,
  useDrawCallback,
  Paint,
  LinearGradient,
  vec,
  Group,
  RoundedRect,
} from "@shopify/react-native-skia";

function GradientCard() {
  const width = 300;
  const height = 200;

  return (
    <Canvas style={{ width, height }}>
      {/* Rounded rectangle with gradient */}
      <RoundedRect x={0} y={0} width={width} height={height} r={16}>
        <LinearGradient
          start={vec(0, 0)}
          end={vec(width, height)}
          colors={["#5469d4", "#9333ea"]}
        />
      </RoundedRect>

      {/* Circle with blur */}
      <Circle cx={150} cy={100} r={60} opacity={0.3}>
        <Paint color="white" />
      </Circle>
    </Canvas>
  );
}

Image Filters and Effects

import {
  Canvas,
  Image,
  useImage,
  BlurMask,
  ColorMatrix,
  Group,
} from "@shopify/react-native-skia";

function ProcessedImage() {
  const image = useImage(require("./photo.jpg"));

  if (!image) return null;

  return (
    <Canvas style={{ width: 300, height: 300 }}>
      <Group>
        {/* Grayscale filter */}
        <ColorMatrix
          matrix={[
            0.21, 0.72, 0.07, 0, 0,
            0.21, 0.72, 0.07, 0, 0,
            0.21, 0.72, 0.07, 0, 0,
            0, 0, 0, 1, 0,
          ]}
        />
        <Image image={image} x={0} y={0} width={300} height={300} fit="cover" />
      </Group>
    </Canvas>
  );
}

Skia Animations (Combined with Reanimated)

import { Canvas, Circle, Fill } from "@shopify/react-native-skia";
import { useSharedValue, withRepeat, withTiming, useDerivedValue } from "react-native-reanimated";

function AnimatedRipple() {
  const radius = useSharedValue(20);

  useEffect(() => {
    radius.value = withRepeat(
      withTiming(80, { duration: 1000 }),
      -1,
      true  // Reverse
    );
  }, []);

  // Convert Reanimated shared value to Skia selector
  const r = useDerivedValue(() => radius.value);

  return (
    <Canvas style={{ width: 200, height: 200 }}>
      <Fill color="white" />
      <Circle cx={100} cy={100} r={r} color="#5469d4" opacity={0.3} />
      <Circle cx={100} cy={100} r={20} color="#5469d4" />
    </Canvas>
  );
}

Feature Comparison

FeatureReanimated v3MotiReact Native Skia
API styleImperativeDeclarativeJSX canvas
Built onNativeReanimatedSkia engine
Custom paths/shapes
Image filters✅ Blur, ColorMatrix
Shaders (GLSL)
Gesture integration✅ NativeVia ReanimatedVia Reanimated
Declarative transitionsManual✅ from/animateManual
Skeleton loadingManual✅ Built-inManual
Presence animationsManual✅ AnimatePresenceManual
Expo SDK✅ Included
Bundle sizeLarge (worklets)Small (thin wrapper)Large (Skia engine)
GitHub stars9k3.7k6.5k

When to Use Each

Choose Reanimated directly if:

  • Complex gesture-driven interactions (swipe, drag, pinch) are core to your UI
  • You need fine-grained control over animation timing and sequencing
  • You're building a custom gesture library or animation system
  • The declarative Moti API doesn't cover your use case

Choose Moti if:

  • Standard UI transitions (fade, slide, scale on mount/unmount) are what you need
  • Skeleton loading screens need to be built quickly
  • You want Framer Motion-like DX — from, animate, exit props
  • Staggered list animations with minimal code

Choose React Native Skia if:

  • You need custom 2D graphics (charts, drawing tools, game-like UI)
  • Image effects (blur, grayscale, color adjustments) are needed
  • Animated backgrounds, gradients, or particle effects
  • Custom paths and shapes that CSS transforms cannot achieve

Performance Profiling and Frame Drop Diagnosis

Understanding why an animation drops frames in React Native requires knowing which thread is the bottleneck. React Native Flipper's Performance panel and the built-in Performance Monitor (shake the device → Show Performance Monitor) display both the JS thread frame rate and the UI thread frame rate separately. If the UI thread shows 60fps but the JS thread drops, your animations are fine but something else in your JavaScript is blocking. If the UI thread drops, the bottleneck is in the native rendering layer — too many views, large Skia canvas draws, or synchronous JS-to-UI bridge calls during animations. Reanimated worklets run on the UI thread, so their animations survive a frozen JS thread. However, calling runOnJS() within a worklet creates a JS thread dependency that can cause hitching if the called function is slow. Profile before optimizing — most apparent performance issues in React Native are caused by slow JavaScript list renders or heavy component trees, not animation code.

Accessibility Considerations for Animated UIs

Animations can be a significant accessibility barrier for users with vestibular disorders or motion sensitivity. iOS and Android both expose a "Reduce Motion" accessibility setting that well-designed apps should respect. Reanimated provides access to this setting via the useReducedMotion() hook — when it returns true, swap spring and timing animations for instant state changes or simpler fade-only transitions. Moti's transition prop can be conditionally set based on this hook, making it easy to provide reduced-motion-friendly variants of your animations. React Native Skia animations require manual reduced-motion handling since they are driven by Reanimated shared values — the same useReducedMotion() check applies. Users who have enabled "Reduce Motion" on their device expect apps to honor that preference; failing to do so causes genuine physical discomfort for some users and reflects poorly on your app's accessibility posture in App Store reviews.

Skia and Reanimated in Production App Size

Adding React Native Skia to your app adds approximately 4–6 MB to the iOS IPA and Android APK due to the Skia native library binary. Reanimated's worklet runtime adds approximately 1–2 MB. These additions are meaningful for apps targeting markets with limited storage devices or slow download connections. Before committing to Skia for visual effects that could be achieved with standard RN transforms, evaluate whether the feature genuinely requires GPU canvas drawing. Gradient card backgrounds, for example, can be achieved with Expo LinearGradient (which uses native gradient APIs without Skia) at a fraction of the bundle size cost. Skia is essential for blur effects (BlurMask), custom shader effects, image color matrix filters, and canvas-based data visualization — these cannot be replicated with standard React Native view styling. The 4–6 MB addition is justified when Skia enables capabilities that would otherwise require a WebView, which adds even more overhead.

Combining Reanimated, Moti, and Skia in a Single App

The three libraries compose cleanly because Moti is built on Reanimated and Skia accepts Reanimated shared values natively. A common production pattern: use Moti for standard UI transitions (screen entry/exit animations, skeleton loaders, toggle states), use Reanimated directly for gesture-driven interactions (swipe-to-delete, drag-to-reorder, pinch-to-zoom), and use Skia for any custom graphics that appear alongside those interactions (chart backgrounds, blur overlays, decorative shape animations). Shared values from Reanimated flow into Skia canvas elements via useDerivedValue(), meaning your gesture handler can simultaneously drive both a React Native view transform (via Reanimated) and a Skia canvas element (via the derived value) at 60fps on the UI thread. This pattern is used in Victory Native XL, which combines all three libraries to power interactive data visualization with gesture-driven chart scrubbing and Skia-rendered line paths.

Methodology

Data sourced from GitHub repositories (star counts as of February 2026), official documentation for all three libraries, bundle size analysis from bundlephobia.com and Expo build output analysis, and performance benchmarks from the React Native community on Twitter/X and the React Native Discord. Moti documentation from nandorojo.com/moti.


Related: NativeWind vs Tamagui vs twrnc for styling libraries that complement animations, or FlashList vs FlatList vs LegendList for list performance in React Native.

See also: React vs Vue and React vs Svelte

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.