shadcn.io is not affiliated with official shadcn/ui
Shaders
ProAgate Geode Shader for React
A polished half geode drawn on the GPU: agate bands in blue-grey, white and violet around a cavity of glinting amethyst points, morphing slowly as the cut moves through the stone. For React with Base UI, when a jewellery, luxury or natural-history page wants a rich, crafted surface behind its copy.
Agate Geode Shader draws a polished half geode, larger than the box. Concentric agate bands in blue-grey, white and violet follow the stone's irregular outline from a rough grey rind at the lower left, sweeping across the left of the box, to a cavity at the upper right lined with amethyst points that catch a moving light. The cut plane travels slowly through the stone, so the bands creep and morph, and a soft polish sheen glides across the slab. A WGSL fragment shader on WebGPU computes it all per pixel: the stone is a distance field warped by 3D simplex noise, the bands are its contours with random widths and shades, and each crystal is a Voronoi cell shaded as a six-sided point.
Use it behind a jewellery, luxury or natural-history hero, or as a stone texture on a card. For marble veins in plain CSS, use the Marble background; for frost, Crystals; for contour bands with no stone or cavity, Smooth Noise Contours Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows CSS rings in the same colors in browsers without WebGPU.
Installation
Usage
import { AgateGeodeShader } from "@/components/ui/agate-geode-shader";<AgateGeodeShader className="flex min-h-[32rem] items-center p-12">
<h1 className="max-w-md text-5xl font-semibold text-slate-950">
Cut from the earth
</h1>
</AgateGeodeShader>Agate Geode Shader is a container: your content sits on top of the stone, 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 stone is pale in both color schemes, so put dark text over it, such as text-slate-950, on the left, away from the cavity.
Every color takes any CSS color and follows a theme switch: bandColor sets the bands (deeper bands are a darker shade of it), bandingColor the thin pale lines and the quartz at the cavity's wall, crystalColor the points, and rindColor the stone's skin.
Demos
Fine bands, citrine
bands={32} packs nearly twice as many, thinner bands between the cavity and the rind. bandColor="#c9a27e" and crystalColor="#d97706" turn the stone into warm agate around a citrine cavity.
Solid agate, in a card
cavity={0} closes the cavity, so the bands ring a small eye at the upper right: pure agate, as a texture for a card.
Performance
About 0.85 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. It keeps quality={1} by default: the bands' thin lines and the crystal edges are line art, and blur at half resolution. 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 rings in the same colors around a violet disc at the upper right, 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 stone 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 bands creep over tens of seconds, and the glints are small facets that brighten and fade as the light circles, once every 25 seconds. On the left half of the box, slate-950 text reads at about 8:1 on half of it and 6:1 or better on three quarters; the darkest bands drop to about 4:1, and the rind's corner is dark, so keep text off the lower left edge.
FAQ
How are the bands drawn?
The stone is a distance field: the distance from the cavity's centre, warped by slow 3D simplex noise into an irregular outline. The bands are its contours, cut into bands layers between the cavity and the rind, each with a random width for its pale line and a random shade, so they read as natural layering rather than a pattern. Inside each band, a fine stripe adds the faint lines real agate has.
What moves?
The cut plane: the noise that shapes the stone is 3D, and the shader slices it slowly deeper, so the bands creep and their outline morphs. A light circles above the cavity, so the amethyst facets glint one after another, and a broad sheen glides across the polished slab.
Can I hide the crystals?
Set cavity={0}: the bands close in on a small eye at the upper right, a solid agate. Up to 0.6, a larger value opens a wider cavity. intensity sets how bright the glints and the sheen are, from 0 (matte) to 2.
Can I make a different stone?
Yes. bandColor, bandingColor, crystalColor and rindColor take any CSS color and follow a theme switch. Warm browns with an orange crystalColor make citrine, pinks make rose quartz, and greens make moss agate. Keep bandingColor lighter than bandColor, so the thin lines read.
API Reference
AgateGeodeShader
data-slot="agate-geode-shader"
bandColor?string
The agate's main bands; deeper bands are a darker shade of it. Any CSS colour. @default "#9fb4c9".
- Default
"#9fb4c9"
bandingColor?string
The thin pale lines between bands, and the quartz at the cavity's wall. Any CSS colour. @default "#f1ede6".
- Default
"#f1ede6"
bands?number
How many bands lie between the cavity and the rind, from 6 to 40. @default 18.
- Default
18
cavity?number
How big the crystal cavity at the upper right is, from 0 (solid agate) to 0.6. @default 0.35.
- Default
0.35
crystalColor?string
The amethyst points in the cavity. Any CSS colour. @default "#8b5cf6".
- Default
"#8b5cf6"
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
How bright the glints on the crystals and the polish sheen are, from 0 to 2. @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.
rindColor?string
The stone's rough outer rind. Any CSS colour. @default "#5d5a58".
- Default
"#5d5a58"
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


Smooth Noise Contours Shader
Patterns & tilingsContour bands with no stone, free
- Pro


Perspex Web Lattice Shader
Tunnels & 3DFaceted cells for a jewellery page
- Pro


Noise Contours Shader
Patterns & tilingsGlossy enamel bands
- Pro


Marbling Shader
Ink, smoke & fireInk veins on paper
- Pro


Drapery Shader
Flow & gradientsSatin for a luxury hero
- Pro


Glazed bands with infinite depth