Standard React Native components are great for buttons, lists, and forms. But the moment you need a custom chart with 2,000 data points, an animated gradient background, or a blur effect that follows your finger — things fall apart pretty quickly. SVG chokes on large datasets. There's no real Canvas API. And if you go the native route, you're writing Swift and Kotlin separately, which... nobody wants to maintain.
React Native Skia fixes this. Built by Shopify and powered by the same Skia engine behind Chrome, Android, and Flutter, it gives you a GPU-accelerated 2D drawing canvas that works across iOS, Android, and the web. Version 2.6.x (the latest as of April 2026) requires React Native 0.79+ and React 19, and it integrates cleanly with Expo SDK 55.
This tutorial covers everything you need to get productive: installation, drawing primitives, Reanimated-powered animations, custom GPU shaders, image filters, gesture-driven graphics, and performance tuning. Every example uses Expo and runs on all platforms.
So, let's dive in.
Installation and Project Setup
Creating a New Expo Project with Skia
The fastest way to get started is Shopify's official Expo template. It preconfigures web support with CanvasKit out of the box:
npx create-expo-app my-skia-app -e with-skia
cd my-skia-app
Adding Skia to an Existing Expo Project
Already have an Expo project? Just install the package directly:
npx expo install @shopify/react-native-skia
One thing to watch out for — Skia uses a postinstall script to copy prebuilt C++ binaries into the right location. If you're using Bun as your package manager, you'll need to add @shopify/react-native-skia to the trustedDependencies array in your package.json.
Platform Requirements
- React Native: 0.79 or higher
- React: 19 or higher
- iOS: 14+
- Android: API level 21+ (API 26+ for video support)
- For older projects: Use
@shopify/[email protected]for RN <= 0.78 and React <= 18
tvOS, macOS, and macOS Catalyst are also supported, which is a nice bonus.
Adding Reanimated for Animations
Honestly, most Skia projects need animations. Install Reanimated alongside Skia:
npx expo install react-native-reanimated
Then add the Reanimated Babel plugin to your babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
The Canvas and Basic Drawing Primitives
Everything in Skia starts with the <Canvas> component. It gives you a 2D Cartesian coordinate system where you control exactly what gets drawn at each pixel.
Drawing Shapes
Skia ships with built-in components for all the common shapes you'd expect:
import { Canvas, Circle, Rect, RoundedRect, Line, vec } from "@shopify/react-native-skia";
export function ShapesDemo() {
return (
<Canvas style={{ width: 300, height: 400 }}>
{/* Filled circle */}
<Circle cx={150} cy={80} r={60} color="#3B82F6" />
{/* Stroked rectangle */}
<Rect
x={40} y={170} width={220} height={80}
color="#EF4444"
style="stroke"
strokeWidth={3}
/>
{/* Rounded rectangle with fill */}
<RoundedRect
x={40} y={280} width={220} height={80}
r={16}
color="#10B981"
/>
{/* Line */}
<Line p1={vec(40, 155)} p2={vec(260, 155)} color="#94A3B8" strokeWidth={2} />
</Canvas>
);
}
Pretty straightforward if you've worked with any drawing API before.
Drawing Paths with SVG Notation
For more complex shapes, use the <Path> component with SVG path strings:
import { Canvas, Path, Skia } from "@shopify/react-native-skia";
export function StarPath() {
const path = Skia.Path.MakeFromSVGString(
"M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 Z"
)!;
return (
<Canvas style={{ width: 256, height: 256 }}>
<Path path={path} color="#F59E0B" />
</Canvas>
);
}
You can also build paths programmatically using Skia.Path.Make() with methods like moveTo, lineTo, cubicTo, and close. This is useful when you need to generate shapes from data at runtime.
Gradients and Fills
import { Canvas, Rect, LinearGradient, RadialGradient, vec } from "@shopify/react-native-skia";
export function GradientDemo() {
return (
<Canvas style={{ width: 300, height: 300 }}>
{/* Linear gradient */}
<Rect x={0} y={0} width={300} height={140}>
<LinearGradient
start={vec(0, 0)}
end={vec(300, 140)}
colors={["#6366F1", "#EC4899"]}
/>
</Rect>
{/* Radial gradient */}
<Rect x={0} y={160} width={300} height={140}>
<RadialGradient
c={vec(150, 230)}
r={100}
colors={["#FBBF24", "#F97316", "#DC2626"]}
/>
</Rect>
</Canvas>
);
}
Animations with Reanimated
This is where Skia really shines. Unlike most React Native animation libraries that need createAnimatedComponent or useAnimatedProps, Skia accepts Reanimated shared values directly as props. No wrappers. No boilerplate.
How It Works Under the Hood
Skia's rendering runs on the UI thread. When you pass a shared value as a prop, Skia reads it directly on the UI thread each frame — the JavaScript thread never gets involved during the animation. That's why Skia animations stay butter-smooth at 60–120 FPS even when your JS thread is busy doing other work.
Basic Animated Circle
import { Canvas, Circle } from "@shopify/react-native-skia";
import { useEffect } from "react";
import { useSharedValue, withRepeat, withTiming, Easing } from "react-native-reanimated";
export function PulsingCircle() {
const radius = useSharedValue(40);
useEffect(() => {
radius.value = withRepeat(
withTiming(80, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
-1, // infinite repeats
true // reverse
);
}, []);
return (
<Canvas style={{ width: 200, height: 200 }}>
<Circle cx={100} cy={100} r={radius} color="#8B5CF6" />
</Canvas>
);
}
Notice that radius (a shared value) goes directly into the r prop — no animated component wrapper required. That simplicity is a huge win when you have dozens of animated properties.
Computed Values with useDerivedValue
Use useDerivedValue when you need to compute animated properties from other shared values:
import { Canvas, Circle, Group } from "@shopify/react-native-skia";
import { useSharedValue, useDerivedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";
export function OrbitAnimation() {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withRepeat(
withTiming(2 * Math.PI, { duration: 3000 }),
-1
);
}, []);
const cx = useDerivedValue(() => 150 + 80 * Math.cos(progress.value));
const cy = useDerivedValue(() => 150 + 80 * Math.sin(progress.value));
const opacity = useDerivedValue(() => 0.5 + 0.5 * Math.sin(progress.value));
return (
<Canvas style={{ width: 300, height: 300 }}>
<Circle cx={150} cy={150} r={8} color="#64748B" />
<Group opacity={opacity}>
<Circle cx={cx} cy={cy} r={20} color="#3B82F6" />
</Group>
</Canvas>
);
}
Animated Path Morphing
Here's something really cool — Skia provides usePathInterpolation to smoothly morph between completely different paths:
import { Canvas, Path, Skia, usePathInterpolation } from "@shopify/react-native-skia";
import { useSharedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";
const trianglePath = Skia.Path.MakeFromSVGString(
"M 128 16 L 240 220 L 16 220 Z"
)!;
const squarePath = Skia.Path.MakeFromSVGString(
"M 40 40 L 216 40 L 216 216 L 40 216 Z"
)!;
const circlePath = Skia.Path.MakeFromSVGString(
"M 128 16 A 112 112 0 1 1 127.99 16 Z"
)!;
export function MorphingShape() {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withRepeat(withTiming(2, { duration: 3000 }), -1, true);
}, []);
const path = usePathInterpolation(
progress,
[0, 1, 2],
[trianglePath, squarePath, circlePath]
);
return (
<Canvas style={{ width: 256, height: 256 }}>
<Path path={path} color="#EC4899" style="fill" />
</Canvas>
);
}
Animated Color Interpolation
Quick heads-up: Skia uses a different internal color format than Reanimated, so you can't use interpolateColor from Reanimated directly. Use interpolateColors from React Native Skia instead:
import { Canvas, Fill, interpolateColors } from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";
const palette = ["#6366F1", "#EC4899", "#F59E0B", "#10B981"];
export function AnimatedBackground() {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withRepeat(
withTiming(palette.length - 1, { duration: 6000 }),
-1,
true
);
}, []);
const color = useDerivedValue(() =>
interpolateColors(
progress.value,
palette.map((_, i) => i),
palette
)
);
return (
<Canvas style={{ width: 300, height: 200 }}>
<Fill color={color} />
</Canvas>
);
}
Custom GPU Shaders with SkSL
Shaders are programs that run on the GPU to compute the color of every single pixel. React Native Skia uses SkSL (Skia Shading Language), which is basically a variant of GLSL. If you've written GLSL for WebGL or ShaderToy, you'll feel right at home — the syntax is nearly identical.
Your First Shader
import { Canvas, Fill, Shader, Skia } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
vec2 uv = pos / vec2(300, 300);
return vec4(uv.x, uv.y, 0.5, 1.0);
}
`)!;
export function GradientShader() {
return (
<Canvas style={{ width: 300, height: 300 }}>
<Fill>
<Shader source={source} />
</Fill>
</Canvas>
);
}
The main function receives each pixel's position and returns an RGBA color. This particular shader creates a smooth gradient based on normalized coordinates — all computed in parallel on the GPU. It's deceptively simple but incredibly powerful.
Uniforms: Passing Data to the GPU
Uniforms let you send dynamic values from JavaScript into the shader. Supported types include float, float2, float3, float4, and matrix types up to float4x4. Arrays work too.
import { Canvas, Fill, Shader, Skia, vec } from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
uniform vec2 center;
uniform float radius;
uniform vec3 ringColor;
vec4 main(vec2 pos) {
float dist = distance(pos, center);
float ring = smoothstep(radius - 4.0, radius, dist)
- smoothstep(radius, radius + 4.0, dist);
return vec4(ringColor * ring, 1.0);
}
`)!;
export function RingShader() {
return (
<Canvas style={{ width: 256, height: 256 }}>
<Fill>
<Shader
source={source}
uniforms={{
center: vec(128, 128),
radius: 80,
ringColor: [0.23, 0.51, 0.96],
}}
/>
</Fill>
</Canvas>
);
}
Animating Shaders with Reanimated
Combine uniforms with useDerivedValue to animate shaders at 60+ FPS. The GPU does the heavy lifting here:
import { Canvas, Fill, Shader, Skia, vec } from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";
const source = Skia.RuntimeEffect.Make(`
uniform float2 iResolution;
uniform float iTime;
half4 main(float2 pos) {
float2 uv = pos / iResolution;
float wave = sin(uv.x * 12.0 + iTime * 3.0) * 0.5 + 0.5;
float gradient = mix(uv.y, wave, 0.6);
return half4(gradient * 0.4, gradient * 0.6, gradient, 1.0);
}
`)!;
export function AnimatedWaveShader() {
const time = useSharedValue(0);
useEffect(() => {
time.value = withRepeat(
withTiming(Math.PI * 2, { duration: 4000 }),
-1
);
}, []);
const uniforms = useDerivedValue(() => ({
iResolution: vec(300, 200),
iTime: time.value,
}));
return (
<Canvas style={{ width: 300, height: 200 }}>
<Fill>
<Shader source={source} uniforms={uniforms} />
</Fill>
</Canvas>
);
}
The shader computation happens entirely on the GPU. The only value crossing from JavaScript is the iTime uniform — a single float per frame. That's it.
Image Filters: Blur, Shadows, and Backdrop Effects
Skia includes a rich set of image filters that operate on rendered pixel data. These are the effects that really make your UI feel polished.
Gaussian Blur
import { Canvas, Image, Blur, useImage } from "@shopify/react-native-skia";
export function BlurredImage() {
const image = useImage(require("./assets/photo.jpg"));
if (!image) return null;
return (
<Canvas style={{ width: 300, height: 300 }}>
<Image image={image} x={0} y={0} width={300} height={300} fit="cover">
<Blur blur={10} mode="clamp" />
</Image>
</Canvas>
);
}
Backdrop Blur (iOS-Style Frosted Glass)
This is probably one of the most-requested effects. Backdrop filters apply effects to the content behind a clipping region — similar to CSS backdrop-filter:
import {
Canvas, Fill, Image, BackdropBlur, RoundedRect, Text, useImage, useFont
} from "@shopify/react-native-skia";
export function FrostedCard() {
const image = useImage(require("./assets/landscape.jpg"));
const font = useFont(require("./assets/Inter-Medium.ttf"), 18);
if (!image || !font) return null;
return (
<Canvas style={{ width: 320, height: 400 }}>
<Image image={image} x={0} y={0} width={320} height={400} fit="cover" />
<BackdropBlur
blur={15}
clip={{ x: 20, y: 240, width: 280, height: 140 }}
>
<RoundedRect x={20} y={240} width={280} height={140} r={16}
color="rgba(255, 255, 255, 0.15)"
/>
</BackdropBlur>
<Text x={40} y={290} text="Frosted Glass" font={font} color="white" />
</Canvas>
);
}
Drop Shadows and Inner Shadows
import { Canvas, RoundedRect, Shadow } from "@shopify/react-native-skia";
export function ShadowCard() {
return (
<Canvas style={{ width: 300, height: 200 }}>
<RoundedRect x={30} y={30} width={240} height={140} r={16} color="white">
<Shadow dx={0} dy={4} blur={12} color="rgba(0,0,0,0.15)" />
<Shadow dx={0} dy={1} blur={3} color="rgba(0,0,0,0.08)" inner />
</RoundedRect>
</Canvas>
);
}
Composing Multiple Filters
You can nest filter components to compose them. Filters apply from innermost to outermost:
<Image image={image} x={0} y={0} width={256} height={256} fit="cover">
<Blur blur={3} mode="clamp">
<ColorMatrix
matrix={[
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 18, -7,
]}
/>
</Blur>
</Image>
Gesture-Driven Graphics
By wiring gesture handlers to shared values, you can create interactive graphics that respond to touch with essentially zero lag. This is one of the things that makes Skia feel so different from other drawing solutions.
import { Canvas, Circle } from "@shopify/react-native-skia";
import { useSharedValue } from "react-native-reanimated";
import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
export function DraggableCircle() {
const cx = useSharedValue(150);
const cy = useSharedValue(150);
const pan = Gesture.Pan().onUpdate((e) => {
cx.value = e.x;
cy.value = e.y;
});
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<GestureDetector gesture={pan}>
<Canvas style={{ flex: 1, backgroundColor: "#F1F5F9" }}>
<Circle cx={cx} cy={cy} r={40} color="#6366F1" />
</Canvas>
</GestureDetector>
</GestureHandlerRootView>
);
}
The gesture data flows from the native gesture handler to the shared value to Skia — all on the UI thread. JavaScript isn't involved at all, which is why this feels instantaneous even on lower-end devices.
Gesture-Driven Shader Uniforms
You can also connect gesture positions directly to shader uniforms for effects like a spotlight or ripple. Here's the key wiring:
const touchX = useSharedValue(150);
const touchY = useSharedValue(150);
const pan = Gesture.Pan().onUpdate((e) => {
touchX.value = e.x;
touchY.value = e.y;
});
const uniforms = useDerivedValue(() => ({
iResolution: vec(300, 300),
iMouse: vec(touchX.value, touchY.value),
}));
Performance: Skia vs SVG vs Canvas
Choosing the right rendering approach matters more than you might think. Here's how the three options compare in practice:
| Factor | React Native Skia | react-native-svg | Canvas API |
|---|---|---|---|
| Rendering engine | GPU via C++/JSI | Native CGContext / Android Canvas | HTML5-style Canvas |
| 1,000+ data points | Smooth 60 FPS | Significant frame drops | Moderate |
| Custom shaders | Full SkSL support | Not supported | Not supported |
| Reanimated integration | Direct prop binding | Requires wrappers | Manual |
| Bundle size impact | ~3–5 MB (Skia binaries) | ~0.5 MB | ~1 MB |
| Learning curve | Moderate to steep | Low (familiar SVG API) | Moderate |
| Best for | Data viz, animations, effects | Icons, logos, static graphics | Basic 2D drawing |
My practical advice: Use standard React Native views for layout, navigation, and content. Embed Skia canvases only where you actually need custom graphics. A typical production screen ends up being 90% React Native views with one or two Skia canvases for charts or visual effects. Don't go overboard.
Performance Tips
- Don't re-create paths on every render. Define
Skia.Pathobjects outside the component or memoize them. This is a common mistake that'll tank your frame rate. - Use
useDerivedValuefor uniforms. This ensures uniform objects are only recomputed when shared values actually change, not on every frame. - Keep Canvas dimensions fixed. Avoid percentage-based sizing that triggers frequent layout recalculations.
- Minimize Canvas count. Each
<Canvas>has overhead. Combine related graphics into a single Canvas whenever possible. - Use
atlasfor many similar sprites. The Atlas API batches draw calls, which is way faster than rendering hundreds of individual components.
Real-World Example: Animated Stats Card
Let's put it all together. Here's a complete example combining several techniques — a stats card with an animated ring chart and a dark background:
import { Canvas, Path, Skia, Fill, BackdropBlur, RoundedRect, Text, useFont }
from "@shopify/react-native-skia";
import { useEffect } from "react";
import { useSharedValue, useDerivedValue, withTiming } from "react-native-reanimated";
function createArcPath(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
const path = Skia.Path.Make();
path.addArc({ x: cx - r, y: cy - r, width: 2 * r, height: 2 * r }, startAngle, endAngle - startAngle);
return path;
}
export function StatsCard({ percentage = 73 }: { percentage?: number }) {
const font = useFont(require("./assets/Inter-Bold.ttf"), 28);
const labelFont = useFont(require("./assets/Inter-Medium.ttf"), 14);
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(percentage / 100, { duration: 1200 });
}, [percentage]);
const arcEnd = useDerivedValue(() => -90 + progress.value * 360);
const animatedPath = useDerivedValue(() => {
return createArcPath(150, 120, 60, -90, arcEnd.value);
});
const label = useDerivedValue(() =>
`${Math.round(progress.value * 100)}%`
);
if (!font || !labelFont) return null;
return (
<Canvas style={{ width: 300, height: 240 }}>
<Fill color="#0F172A" />
{/* Background ring */}
<Path
path={createArcPath(150, 120, 60, 0, 360)}
style="stroke"
strokeWidth={10}
color="rgba(255,255,255,0.1)"
strokeCap="round"
/>
{/* Animated progress ring */}
<Path
path={animatedPath}
style="stroke"
strokeWidth={10}
color="#3B82F6"
strokeCap="round"
/>
<Text x={125} y={130} text={label} font={font} color="white" />
<Text x={110} y={155} text="Completed" font={labelFont} color="#94A3B8" />
</Canvas>
);
}
Skia Backends: Ganesh and Graphite
React Native Skia currently ships with two GPU backends:
- Ganesh — the default, stable backend. This is what you should use in production.
- Graphite — an experimental next-generation backend available in the
@nextnpm channel. It uses modern GPU APIs (including Dawn, Google's WebGPU implementation) and promises automatic threading and seamless 2D/3D composition. It requires Android API level 26+ and is not recommended for production as of April 2026.
For most projects, stick with Ganesh. But keep an eye on Graphite — it's going to be a big deal once it stabilizes.
Frequently Asked Questions
How do I enable React Native Skia in Expo?
Run npx expo install @shopify/react-native-skia in your Expo project. If you're starting from scratch, use npx create-expo-app my-app -e with-skia to get a preconfigured template with web support. Skia works with Expo SDK 53 and newer. No custom native code or config plugins are needed for basic usage.
Is React Native Skia compatible with the New Architecture?
Yes — and it actually works better with it. React Native Skia v2.x is fully compatible with Fabric and TurboModules. The move to Fabric is what delivered the major performance improvements: up to 50% faster on iOS and nearly 200% faster on Android. The library uses an immutable display list that eliminates concurrency issues with the new concurrent renderer.
Can I use React Native Skia for charts instead of a charting library?
You can, but think about the trade-off. Skia gives you complete control and the best performance for large datasets (thousands of data points). However, you'll need to draw every axis, label, and data point yourself. For standard charts, libraries like Victory Native (which actually uses Skia under the hood) give you a higher-level API with much less effort. Use raw Skia when you need custom visualizations that no charting library supports.
Does React Native Skia work on the web?
Yes. Skia compiles to WebAssembly via CanvasKit and runs in the browser. Use the Expo Skia template or run yarn setup-skia-web to copy the canvaskit.wasm file into your project's public folder. You can also load CanvasKit from a CDN to reduce bundle size. Web performance is excellent for complex scenes, outperforming both SVG and standard 2D Canvas.
How much does React Native Skia add to my app's bundle size?
The Skia native binaries add roughly 3–5 MB to your app (varies by platform and architecture). That's a meaningful increase for very size-sensitive apps. If all you need are simple icons or static SVGs, react-native-svg is lighter. But if you need custom graphics, animations, or shaders, the trade-off is absolutely worth it.