React Native Reanimated vs Moti vs React Native Skia: Animation 2026
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
| Feature | Reanimated v3 | Moti | React Native Skia |
|---|---|---|---|
| API style | Imperative | Declarative | JSX canvas |
| Built on | Native | Reanimated | Skia engine |
| Custom paths/shapes | ❌ | ❌ | ✅ |
| Image filters | ❌ | ❌ | ✅ Blur, ColorMatrix |
| Shaders (GLSL) | ❌ | ❌ | ✅ |
| Gesture integration | ✅ Native | Via Reanimated | Via Reanimated |
| Declarative transitions | Manual | ✅ from/animate | Manual |
| Skeleton loading | Manual | ✅ Built-in | Manual |
| Presence animations | Manual | ✅ AnimatePresence | Manual |
| Expo SDK | ✅ Included | ✅ | ✅ |
| Bundle size | Large (worklets) | Small (thin wrapper) | Large (Skia engine) |
| GitHub stars | 9k | 3.7k | 6.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,exitprops - 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.