Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

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

Framework

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.

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

Preview width

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.

Preview width

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.

Will it slow my page down?

It costs about 0.85 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. Keep quality at 1 if you can: the band lines and crystal edges soften at half resolution.

What happens in browsers without WebGPU?

The box keeps its CSS still: rings of the band colors around a violet disc at the upper right, fading to the rind and dark stone at the far corner. Nothing errors: the root's data-state reads unsupported.

API Reference

AgateGeodeShader

data-slot="agate-geode-shader"

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

All 101 shaders