Skip to main content

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

·PkgPulse Team

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

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.