shadcn.io is not affiliated with official shadcn/ui
Marbling Shader draws paper marbling, the craft of floating ink on water and printing it. Every few seconds a drop of ink lands on cream paper in concentric rings of indigo, oxblood and ochre, and pushes the older drops aside; then an invisible comb drags through everything, back and forth and up and down, pulling the rings into feathers and chevrons. A WGSL fragment shader on WebGPU computes every pixel by undoing each drop and each comb stroke in turn, with the drop and tine transforms from Jaffer and Lu's Mathematical Marbling (IEEE CG&A, 2012), so the edges stay crisp and flat, like a print.
Use it behind a stationery, bookbinding, paper-goods or editorial hero, or as an endpaper-style band between sections. It sits on a light ground, unlike most shaders. For one curl of smoke on a dark ground, use Incense Shader; for ink sinking in water, Ink Vortex Shader; for stone veins in plain CSS, the Marble background. It runs only while it's on screen, holds a still frame with reduced motion, and shows CSS rings in the same inks in browsers without WebGPU.
Installation
Usage
import { MarblingShader } from "@/components/ui/marbling-shader";<MarblingShader className="flex min-h-[32rem] items-center justify-center">
<div className="rounded-xl bg-[#f1e8d6]/85 px-8 py-6">
<h1 className="text-5xl font-semibold text-stone-900">Hand bound</h1>
</div>
</MarblingShader>Marbling Shader is a container: your content sits on top of the paper, 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 pattern is busy and runs from cream to deep indigo, so no text color reads on it bare: put copy on a panel of the paper color, like the bg-[#f1e8d6]/85 one above, which keeps dark stone text above 10:1 everywhere.
Demos
Black ink
colors takes three inks; here black, the paper's own white and a grey, so drops ring black and grey with wide bands of white between, on paperColor="#f7f5f0". rings={5} gives each drop its most rings, and comb={0.15} barely drags them, so they stay round, nested stones, closer to Japanese suminagashi than to combed Turkish ebru.
In a card
The drops are sized to the box's height, so a card shows fewer of them. drops={10} keeps more on the paper at once and rings={2} makes each one two broad bands, so the pattern reads at card size.
Performance
About 0.7 ms of GPU time a frame at 1280×720 on an M2 Max laptop with the default 8 drops; slower GPUs take several times longer. The cost grows with drops, since each pixel undoes more events, and stops early where a newer drop covers the point. It keeps full quality: the ring edges are crisp, and half resolution would soften them. 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 CSS drops of rings on the paper, in your colors and paperColor, 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 pattern is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame, combed drops across the paper, and stops. Nothing flashes: a drop spreads over two seconds and a comb pass over two and a half. The contrast inside the pattern is high and changes everywhere, so never set text on it bare: a bg-[#f1e8d6]/85 panel keeps text-stone-900 above 10:1.
FAQ
Can I change the inks?
Yes: colors takes three CSS colors, and each drop rings them in turn with a paper-colored ring between, starting at a random one. paperColor sets the ground and those pale rings. They take any CSS color, var(--primary) included, and follow a theme switch. Pass the paper color as one of the three inks for more white between the rings, as the Black ink demo does.
What does comb do?
comb sets how hard the comb drags the ink. At 0 the drops only push each other into nested, stone-like rings (what marblers call a stone pattern); at 0.6 they pull into feathers and chevrons; at 1 the feathers grow long and fine. drops sets how many drops float on the paper at once, from 4 to 12, and rings the rings in each, from 1 (solid) to 5.
How does it work?
Marbling has two moves, and both have exact inverses. A drop of radius r at c pushes every point outward, keeping area: a point at distance d moves to √(d² + r²). A tine dragged along a line shifts every point along it, most near the tine. To color a pixel, the shader starts from the newest event and walks back through the last drops and comb strokes, undoing each. The first drop that contains the point gives its color, by which ring it falls in; a point outside every drop is bare paper. The newest drop spreads as it lands, the comb's pull grows over its pass, and the oldest drop's rings narrow to hairlines and vanish, so nothing pops. The shader carries how far each transform stretches the pixel back through the walk, which gives every ring edge exactly one pixel of anti-aliasing.
How do I make it faster or slower?
speed scales time: at 1 a drop lands or the comb passes every two and a half seconds, at 0.5 every five. paused holds the frame, and time draws one fixed moment, in seconds, which is handy for a poster.
API Reference
MarblingShader
data-slot="marbling-shader"
colors?[string, string, string]
The three inks, as CSS colours; drops ring them in turn, with paper-coloured rings between. @default ["#1f2a5a", "#8c2f2f", "#d39b3a"].
- Default
DEFAULT_COLORS
comb?number
How hard the comb drags the ink, from 0 (round drops only) to 1 (deep feathers). @default 0.6.
- Default
0.6
drops?number
How many drops float on the paper at once, from 4 to 12. @default 8.
- Default
8
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.
intensity?number
Ink strength, from 0 (bare paper) to 2 (deeper than the colours). @default 1.
- Default
1
interactive?boolean
Whether u.pointer follows the pointer. @default false.
paperColor?string
The ground, which also makes the pale rings. Any CSS colour. @default "#f1e8d6".
- Default
"#f1e8d6"
paused?boolean
Holds the current frame. @default false.
quality?number
Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.
rings?number
Rings in each drop, from 1 (solid drops) to 5. @default 3.
- Default
3
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


Ink Vortex Shader
Ink, smoke & fireInk sinking in water, on a light ground
- Pro


Incense Shader
Ink, smoke & fireOne curl of smoke on a dark ground
- Pro


Warped Noise Shader
Flow & gradientsInk-in-milk swirls with no comb
- Pro


CMYK Halftone Shader
Digital & retroAnother printed look, in process inks
- Pro


Burning Paper Shader
Ink, smoke & fireCream paper with its edges on fire
- Pro


Simplex Truchet Weave Shader
Patterns & tilingsA crafted pattern of woven cord