Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

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

Framework

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.

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

Preview width

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.

Preview width

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.

Will it slow my page down?

It costs about 1.2 ms of GPU time a frame at 1280×720 on a laptop at its default half resolution, and the CPU only sets a few numbers each frame. It stops when it's off screen. quality={1} renders every pixel, at about 2.5 times the cost, for no visible gain on soft ink.

What happens in browsers without WebGPU?

The box keeps its CSS still, two soft ellipses of ink under a bright top, in your colors, so the page still looks finished. Nothing errors: the root's data-state reads unsupported.

API Reference

InkVortexShader

data-slot="ink-vortex-shader"

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

All 101 shaders