Skip to content

Skia

@three-flatland/skia brings Skia’s GPU-accelerated vector rendering to Three.js via WASM, built with Zig. It auto-detects WebGPU or WebGL from your renderer and supports both Three.js and React Three Fiber.

Includes a native WebGPU backend (Graphite/Dawn) — CanvasKit’s npm package ships WebGL only. At 857 KB (WebGPU) / 1 MB (WebGL) brotli vs CanvasKit’s 2.2 MB.

Installation

Terminal window
npm install @three-flatland/skia@alpha

Peer dependencies: three >= 0.183.1. For React: @react-three/fiber >= 10.0.0-alpha.2 and react >= 19.0.0.

WASM Setup

Skia ships ~1 MB WASM binaries (one for WebGL, one for WebGPU). Vite handles this automatically in dev — no config needed. For production builds or other bundlers, copy the WASM to your public directory:

Terminal window
npx skia-wasm public/skia

This copies skia-gl.wasm and skia-wgpu.wasm to public/skia/. Then tell Skia where to find them:

// Option 1: pass the URL directly
const skia = await Skia.init(renderer, { wasmUrl: '/skia/skia-gl.wasm' })
// Option 2: set env vars (replaced at build time by your bundler)
// webpack.config.js
const { DefinePlugin } = require('webpack')
module.exports = {
plugins: [
new DefinePlugin({
'process.env.SKIA_WASM_URL_GL': JSON.stringify('/skia/skia-gl.wasm'),
'process.env.SKIA_WASM_URL_WGPU': JSON.stringify('/skia/skia-wgpu.wasm'),
}),
],
}

Initialization

import { Skia } from '@three-flatland/skia'
import { SkiaCanvas, SkiaRect } from '@three-flatland/skia/three'
// Auto-detects WebGL or WebGPU from the renderer
const skia = await Skia.init(renderer)
const canvas = new SkiaCanvas({ renderer, width: 512, height: 512 })
canvas.add(new SkiaRect())
// In your animation loop:
canvas.invalidate()
canvas.render(renderer)

SkiaCanvas

The root node that owns the render target and walks children to draw. Two modes:

ModeDescriptionUse Case
TextureRenders to an off-screen texture (canvas.texture)Apply Skia output to 3D mesh materials
OverlayRenders to the screen framebufferHUD, debug text, UI on top of 3D
// Texture mode (default)
const shapes = new SkiaCanvas({ renderer, width: 1024, height: 880 })
// Overlay mode
const overlay = new SkiaCanvas({
renderer,
width: window.innerWidth * dpr,
height: window.innerHeight * dpr,
overlay: true,
})

Render Pipeline

SkiaCanvas does not auto-render. Call render() each frame — it automatically marks the canvas dirty and draws:

function animate() {
// 1. Render Skia shapes to texture
shapesCanvas.render()
// 2. Apply texture to mesh
material.map = shapesCanvas.texture
// 3. Render 3D scene
renderer.render(scene, camera)
// 4. Overlay on top
overlay.render()
}

Drawing Nodes

All nodes extend SkiaNode (which extends Three.js Object3D) and accept inline paint props.

Common Paint Props

PropTypeDescription
fill[r, g, b, a]Fill color (0-1 floats)
stroke[r, g, b, a]Stroke color
strokeWidthnumberStroke width in pixels
opacitynumber0-1 opacity
blurnumberGaussian blur sigma
blendModeBlendModeCompositing blend mode
paintSkiaPaintExplicit paint (overrides all above)

Shape Nodes

{/* Rectangle with rounded corners */}
<skiaRect x={10} y={10} width={200} height={100}
cornerRadius={12} fill={[0.2, 0.4, 1, 1]} />
{/* Circle */}
<skiaCircle cx={150} cy={150} r={60} fill={[1, 0.3, 0.3, 1]} />
{/* Ellipse */}
<skiaOval x={100} y={100} width={200} height={120}
stroke={[1, 0.8, 0.2, 1]} strokeWidth={3} />
{/* Line */}
<skiaLine x1={10} y1={10} x2={200} y2={100}
stroke={[1, 1, 1, 0.5]} strokeWidth={2} />

Path Node

Accepts SVG path data or an explicit SkiaPath reference:

{/* From SVG data string */}
<skiaPathNode d="M10 80 Q95 10 180 80" fill={[1, 1, 1, 1]} />
{/* From SkiaPath reference (for dynamic paths, boolean ops) */}
<skiaPathNode path={myPath} fill={[0.9, 0.3, 0.3, 0.9]} />

Text Node

Requires a SkiaFont loaded via SkiaFontLoader:

<skiaTextNode text="Hello Skia" font={font}
fill={[1, 1, 1, 1]} x={20} y={40} />

SkiaGroup

Groups apply 2D transforms, clipping, and layer effects to all children:

<skiaGroup position={[100, 50, 0]} scale={[2, 2, 1]}>
<skiaRect fill={[1, 0, 0, 1]} width={50} height={50} />
</skiaGroup>

Uses standard Object3D position, scale, and rotation.z (radians). The degrees prop is a convenience shorthand.

PropDescription
positionTranslation (Object3D — [x, y, z])
rotationRotation (Object3D — rotation.z in radians)
degreesRotation in degrees (convenience, overrides rotation.z)
scaleScale (Object3D — [x, y, z])
skewX, skewYSkew (Skia-specific, no Object3D equivalent)
opacityLayer opacity
blurGaussian blur on group
shadowDrop shadow ({ dx, dy, blur, color })
clipRect[x, y, w, h] clip rectangle
backdropBlurFrosted glass effect

Font Loading

SkiaFontLoader loads a SkiaTypeface — the font data independent of size. Call .atSize(n) to get a sized SkiaFont. The typeface is cached by URL, and sized fonts are cached internally — so multiple .atSize() calls are cheap.

import { SkiaFontLoader } from '@three-flatland/skia/three'
const typeface = await SkiaFontLoader.load('/fonts/Inter.ttf')
const titleFont = typeface.atSize(32)
const bodyFont = typeface.atSize(14)
textNode.font = titleFont

The typeface resolves the SkiaContext lazily — if no context is available when atSize() is called, it falls back to the global SkiaContext.instance singleton. Inside a <SkiaCanvas> subtree, this is always available.

Custom Paints

For gradients, shaders, and effects beyond flat fill/stroke, create a SkiaPaint:

import { SkiaPaint } from '@three-flatland/skia'
const paint = new SkiaPaint(skia)
.setFill()
.setLinearGradient(0, 0, 200, 0,
[0xFFFF0000, 0xFF0000FF], [0, 1])
rect.paint = paint // overrides inline fill/stroke props

In React, use useState initializer to create paints that survive strict mode:

const skia = useSkiaContext()!
const [paint] = useState(() => new SkiaPaint(skia).setFill())
useFrame(({ elapsed }) => {
// Animate the gradient each frame
paint.setLinearGradient(...)
})
return <skiaRect paint={paint} x={0} y={0} width={200} height={50} />

Path Operations

Create paths programmatically and perform boolean operations:

import { SkiaPath } from '@three-flatland/skia'
const a = new SkiaPath(skia).addCircle(100, 100, 40)
const b = new SkiaPath(skia).addCircle(130, 100, 40)
const result = new SkiaPath(skia)
a.opInto(b, 'intersect', result) // 'difference' | 'intersect' | 'union' | 'xor'
pathNode.path = result

Operations: difference, intersect, union, xor, reverse-difference.

Texture Attachment (React)

attachSkiaTexture is an R3F attach helper that grabs .texture from a SkiaCanvas and assigns it to the parent material’s map:

import { SkiaCanvas, attachSkiaTexture } from '@three-flatland/skia/react'
<meshBasicMaterial transparent premultipliedAlpha>
<SkiaCanvas attach={attachSkiaTexture} renderer={renderer}
width={1024} height={880}>
{/* Skia shapes rendered to texture */}
</SkiaCanvas>
</meshBasicMaterial>

Next Steps