shadcn.io is not affiliated with official shadcn/ui
Minimal Jigsaw Shader draws an assembled jigsaw puzzle that drifts slowly across the box. Every piece is a square from a grid; each of its four edges gets one round knob on a narrow neck, sticking out one way or the other at random, and the neighbour on that side is cut to take it. Each piece is a flat color picked from a palette, and the cuts are thin, crisp lines. A WGSL fragment shader on WebGPU computes every pixel from the grid, so the outlines stay sharp at any size.
Use it behind a playful hero, an onboarding or "you're all set" step, or a team or partners page, where fitting together is the point. For a quieter grid with no knobs, in plain CSS, use the Grid Pattern background; for a pattern of curves that join up from tile to tile, Truchet Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a CSS grid of the same colors in browsers without WebGPU.
Installation
Usage
import {
MinimalJigsawShader,
} from "@/components/ui/minimal-jigsaw-shader";<MinimalJigsawShader className="flex min-h-[32rem] items-center justify-center">
<div className="rounded-xl bg-white/80 px-8 py-6">
<h1 className="text-5xl font-semibold text-stone-950">Ship faster</h1>
</div>
</MinimalJigsawShader>Minimal Jigsaw Shader is a container: your content sits on top of the puzzle, in normal flow. It fills its own box, so give it a height or content. For a backdrop behind the whole page, render it empty with className="fixed inset-0 -z-10". The default pastels are light in both color schemes, so use dark text: every pastel gives black text above 14:1. The cut lines run through the letters, though, so a large headline reads bare, while smaller copy wants a panel: bg-white/80 keeps black text above 13:1 everywhere.
Demos
Earth
palette="earth" switches to terracotta, sage, sand, pine and rose. outlineColor="#fbf7f0" draws the cuts in cream, like gaps between pieces on a table, and knobSize={1.2}, the largest, gives every piece bigger knobs.
In a card
pieceColors replaces the palette with your own colors, here four shades of indigo: pass one to five, and they repeat to fill five. pieces={5} fits five pieces to the height of the card, so they stay big enough to read as puzzle pieces. It keeps full quality: the cut lines are thin and blur at half resolution, and a card costs the GPU next to nothing.
Performance
About 0.2 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. Each pixel checks its own grid square and the four knobs on its edges, with a few integer hashes and circle distances. Keep quality at 1: the lines are a pixel or two wide. The animation stops when the shader scrolls out of view, and the browser pauses it in a hidden tab. Every shader on a page shares one GPU device and one frame loop, and vgpu (about 50 KB gzipped) loads only when the first one mounts.
Browser support
The shader needs WebGPU: Chrome and Edge, Safari 26 and later, and Firefox on Windows and macOS. Elsewhere, and until the first frame, the box shows a CSS grid of squares in the first four piece colors, with the same outlines, so the layout never jumps. data-state on the root reads unsupported there, if you want to style around it. See Shader Canvas.
Accessibility
The puzzle is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame and stops. Nothing flashes: the colors never change, and the puzzle drifts about a piece every six seconds. Every pastel gives black text above 14:1, but the cut lines cross your letters, so put body text on a bg-white/80 panel, which keeps black text above 13:1.
FAQ
How are the pieces drawn?
From one square grid. Each edge between two squares gets a round knob, a circle whose center sits just past the edge, so the edge cuts it into a narrow neck; a hash of the edge decides which way it points. A pixel belongs to its own square unless it's inside a neighbour's knob, and the outline is drawn where that changes.
Do the pieces really interlock?
Yes. Every knob is cut out of the piece it pokes into, so each piece's tabs fit its neighbours' blanks exactly, and no two knobs overlap, even at the largest knobSize.
How do I change the size of the pieces?
pieces sets how many fit the height of the box, from 4 to 16. They scale with the box, so a card shows the same number as a hero: lower it for a small box. knobSize sets the knobs, from 0.6 to 1.2.
Which palettes are there, and can I use my own colors?
palette picks one of four: pastel (the default), vivid, earth and mono. pieceColors replaces it with your own, one to five CSS colors, var(--primary) included, and they follow a theme switch. outlineColor sets the cut lines: dark on light pieces, or light for a cut-paper look on dark ones.
Credits
Inspired by Minimal Jigsaw (opens in a new tab) by Shane on Shadertoy: a jigsaw pattern from a grid and a round knob per edge. This shader is our own code and design, written from scratch: square pieces with necked knobs, rounded where they meet the edge, set palettes and your own colors, and a CSS-pixel outline.
API Reference
MinimalJigsawShader
data-slot="minimal-jigsaw-shader"
fallback?string
What shows until the first frame and in browsers without WebGPU: a CSS
background value (gradients, sizes, a colour) that looks like the
shader. It can use the colour props' --shader-<field> variables.
interactive?boolean
Whether u.pointer follows the pointer. @default false.
knobSize?number
How big the knobs are, from 0.6 to 1.2. @default 1.
- Default
1
outlineColor?string
The cut lines between pieces. Any CSS color. @default "#1c1917".
- Default
"#1c1917"
palette?keyof typeof PALETTES
The set of colors the pieces pick from. @default "pastel".
- Default
"pastel"
paused?boolean
Holds the current frame. @default false.
pieceColors?readonly string[]
Your own piece colors, one to five CSS colors: they replace the palette.
pieces?number
How many pieces fit the height of the box, from 4 to 16. @default 7.
- Default
7
quality?number
Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.
speed?number
Animation speed: 1 is normal, 0 holds the frame. @default 1.
time?number
A fixed moment, in seconds: draws it and doesn't animate.
render?ReactElement | ((props: object, state: object) => ReactElement)
Replace the host element or compose it with another component.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Forwarded to the part.
Related
- Pro


Truchet Shader
Patterns & tilingsCurves that join from tile to tile
- Pro


Triangle Mesh Incircles Shader
Patterns & tilingsPlayful geometry that flexes
- Pro


Random Grid Subdivision Shader
Patterns & tilingsGrid cells cut into shards of glass
- Pro


Isometric Blocks Shader
GeometryBlocks stacked into a city
- Pro


Voxel Flyover Shader
Water & landscapesA playful world of blocks
- Pro


Breathing Dots Shader
GeometryCalm, living texture for an empty state