React Native Skia: GPU-Accelerated Graphics, Shaders, and Animations with Expo

Learn React Native Skia from scratch — GPU-accelerated drawing, Reanimated animations, custom SkSL shaders, image filters, and gesture-driven graphics with full Expo support.

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.Path objects outside the component or memoize them. This is a common mistake that'll tank your frame rate.
  • Use useDerivedValue for 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 atlas for 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 @next npm 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.

About the Author Editorial Team

Our team of expert writers and editors.