shadcn.io is not affiliated with official shadcn/ui
Shaders
ProInk Vortex Shader for React
Ink rings sinking through a backlit tank, drawn on the GPU: each drop rolls into a smoky ring that widens and breaks into drooping lobes, Prussian blue on off-white. For React with Base UI, when a design-studio, fragrance or editorial hero wants slow, physical motion on a light ground.
Ink Vortex Shader draws drops of ink falling into a clear tank lit from behind. Each one rolls into a vortex ring that sinks, slows and widens, trailing a thin thread; as it ages, its rim swells into lobes that droop and tear, the start of the smaller rings real ink breaks into. Two rings at a time sink at different depths, Prussian blue at their cores and soft grey at their edges, on off-white water. A WGSL fragment shader on WebGPU ray marches each ring as a volume of noisy ink and lets the light through it by how much ink it crosses, so the rings read as smoke in water rather than as flat shapes.
Use it behind a design-studio, fragrance, gallery or editorial hero that wants one slow, physical motion on a light ground. For ink floated on paper and combed, use Marbling Shader; for a single thread of smoke on charcoal, Incense Shader; for ink plumes in plain CSS, the Ink background. It's heavy, so it renders at half resolution by default. It runs only while it's on screen, holds a still frame with reduced motion, and shows soft gradients of the same ink in browsers without WebGPU.
Installation
Usage
import { InkVortexShader } from "@/components/ui/ink-vortex-shader";<InkVortexShader className="flex min-h-[32rem] items-center justify-center">
<div className="rounded-xl bg-[#f4f1ea]/80 px-8 py-6">
<h1 className="text-5xl font-semibold text-slate-900">Studio Nine</h1>
</div>
</InkVortexShader>Ink Vortex Shader is a container: your content sits on top of the water, 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 water is off-white in both color schemes, so use dark text. Most of the box is clear water, but a ring sinks through wherever it likes, so put copy on a panel of the water color, like the bg-[#f4f1ea]/80 one above, which keeps text-slate-900 above 10:1 everywhere.
Demos
Glow on black
emissive makes the ink glow instead of absorbing the light, for dark pages: here cyan ink in near-black water, with backlightColor turned down to a deep navy so the top of the tank only faintly lifts.
In a card
The tank scales with its box, and the rings with its height. intensity={0.8} thins the ink a little, so a small surface stays light.
Performance
About 1.2 ms of GPU time a frame at 1280×720 on an M2 Max laptop at the default quality={0.5}, and 2.9 ms at full resolution; slower GPUs take several times longer. On a 2× screen the default renders as many pixels as full resolution does on a 1× one, so about 2.9 ms. The rings are soft, so half resolution looks the same. Each pixel marches only the rings its ray passes near, takes big steps through clear water and small ones inside the ink, at most 40, so the empty parts of the tank cost little. 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 two soft ellipses of ink on the backlit water, in your colors, 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 ink is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame, a young ring above an older one breaking into lobes, and stops. Nothing flashes: a ring takes 12 seconds to sink through the tank and fades in and out over more than a second. Dark text on a bg-[#f4f1ea]/80 panel stays above 10:1; bare, it drops to nothing where a ring's core passes behind it.
FAQ
How is it drawn?
Each ring is a torus: its distance field, softened into a cloud of ink and roughened by 3D simplex noise as it ages. Every pixel casts a ray into the tank and steps through each ring it passes near, adding up how much ink it crosses. The light reaching the eye falls off with that amount (the Beer–Lambert law), each color channel by how much the ink absorbs it, so thin ink tints and thick ink darkens. There's no light march and no simulation: the rings' paths, widths and lobes are functions of their age.
How do I change how many rings sink at once?
rings sets it, from 1 to 3. Each ring keeps to its own lane across the box and is born every 12 seconds at a new spot in it, so 1 gives a single, calm ring and 3 a busier tank. turbulence sets how smoky and torn they get as they age, from 0 (smooth) to 1.
How do I use it on a dark page?
Set emissive, a dark waterColor and a dark backlightColor, and a light inkColor: the ink then glows instead of absorbing, like the Glow on black demo. Without emissive, a dark ink in dark water barely shows.
Can I change the colors?
Yes: inkColor is the ink, waterColor the water at the edges and bottom, and backlightColor the light behind the tank, brightest at the top middle. They take any CSS color, var(--primary) included, and follow a theme switch. intensity sets how dense the ink is, from 0 to 2.
API Reference
InkVortexShader
data-slot="ink-vortex-shader"
backlightColor?string
The light behind the tank, brightest at the top middle; keep it dark with emissive. Any CSS colour. @default "#ffffff".
- Default
"#ffffff"
emissive?boolean
Makes the ink glow instead of absorbing the light, for a dark waterColor. @default false.
- Default
false
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.
inkColor?string
The ink. Any CSS colour. @default "#14305c".
- Default
"#14305c"
intensity?number
How dense the ink is, from 0 (none) to 2 (inky). @default 1.
- Default
1
interactive?boolean
Whether u.pointer follows the pointer. @default false.
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.
- Default
0.5
rings?number
How many rings sink at once, from 1 to 3. @default 2.
- Default
2
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.
turbulence?number
How smoky and torn the rings get as they age, from 0 (smooth) to 1. @default 0.6.
- Default
0.6
waterColor?string
The water, at the edges and the bottom of the tank. Any CSS colour. @default "#f4f1ea".
- Default
"#f4f1ea"
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


Marbling Shader
Ink, smoke & fireInk floated on paper and combed
- Pro


Incense Shader
Ink, smoke & fireA single thread of smoke on charcoal
- Pro


Smoke Shader
Ink, smoke & fireBroad plumes rising on dark
- Pro


Warped Noise Shader
Flow & gradientsInk folding through milk, flat and lighter
- Pro


Chiffon Shader
Flow & gradientsSlow, airy layers on a light page
- Pro


Glass Caustics Shader
Light & glassAnother slow, physical light on cream