Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

Marbling Shader for React

Paper marbling drawn on the GPU: drops of indigo, oxblood and ochre ink land in rings on cream paper and a comb drags them into feathers. For React with Base UI, when a stationery, bookbinding or editorial page wants a crafted, printed surface that slowly changes.

Framework

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.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

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.

Preview width

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.

Preview width

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.

Will it slow my page down?

It costs about 0.7 ms of GPU time a frame at 1280×720 on a laptop, and the CPU only sets a few numbers each frame. It stops when it's off screen. On a slow GPU, lower drops before quality: half resolution saves GPU time but softens the crisp ring edges.

What happens in browsers without WebGPU?

The box keeps its CSS still, a few round drops of rings on the paper, in your colors, so the page still looks finished. Nothing errors: the root's data-state reads unsupported.

API Reference

MarblingShader

data-slot="marbling-shader"

PropType
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.

All 101 shaders