Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

Poincaré Disc Animation Shader for React

A hyperbolic tiling in a Poincaré disc: rings of glass tiles that shrink forever toward a gold rim, with pulses of light running between them. For React with Base UI, when a math, science or education page wants geometry that feels infinite.

Framework

Poincaré Disc Animation Shader draws a tiling of the hyperbolic plane in a Poincaré disc: triangles meeting eight at each corner, every one the same size in hyperbolic terms, so they shrink without end toward a thin gold rim. The tiles are stained glass, domed toward the middle and leaded in near-black, and their color steps from ring to ring, gold and amber at the center through rose and violet to blue at the edge, drifting slowly outward. A network of grooves joins the tiles' centers, and pulses of light run along it, out of every other ring and into the next. The disc turns once every 50 seconds, and beyond it the same tiling echoes, inverted and dim. A WGSL fragment shader on WebGPU folds every pixel into one triangle with mirror reflections and circle inversions, each frame.

Use it behind a math, science, education or research page, or as a centerpiece that rewards a second look. For a 2D kaleidoscope of soft waves instead, use Kaleidoscope Shader; for another infinite pattern, the decorated fractal of Mandelbrot Pattern Decoration Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a CSS disc of 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 {
  PoincareDiscAnimationShader,
} from "@/components/ui/poincare-disc-animation-shader";
<PoincareDiscAnimationShader className="flex min-h-[32rem] items-center justify-center">
  <div className="rounded-full bg-black/60 px-8 py-4 backdrop-blur">
    <h1 className="text-4xl font-semibold text-white">Ship faster</h1>
  </div>
</PoincareDiscAnimationShader>

Poincaré Disc Animation Shader is a container: your content sits on top of the disc, 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 disc is centered and fills 90% of the box's shorter side, so it suits square and wide boxes alike. The tiles are bright and busy, so put text on a dark glass panel, like the bg-black/60 pill above.

Demos

Squares

tiling="4-6" switches to squares meeting six at each corner. hueStep={1.8} moves the color further from one ring to the next, spin={0} stops the turning, and interactive lets the pointer slide the plane: move it toward the rim, and the tiles under it slide toward the middle and grow, as if you walked across the hyperbolic plane.

Preview width

In a card

A square card fits the whole disc. dashSize={1.4} lengthens the pulses, so they read at card size.

Preview width

Performance

About 0.3 ms of GPU time a frame at 1280×720 on an M2 Max laptop, even with up to 24 inversions for pixels near the rim; slower GPUs take several times longer. It keeps full quality: the lead lines blur at half resolution, and the cost is already low. 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 a CSS disc, gold at the center through rose and violet to blue, inside a gold ring on a dark field, 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 disc is decoration: it's hidden from assistive tech and lets every click through to your content. With interactive, it follows a mouse or pen, never touch. With reduced motion it draws one still frame and stops. Nothing flashes: the pulses are small and steady, the disc turns once every 50 seconds, and the colors take about 33 seconds to cycle. Text needs a panel: on the bare tiles white text drops to about 2.4:1, while on a bg-black/60 panel it keeps above 5.7:1 everywhere over the middle of the disc, through the whole color cycle.

FAQ

What is the Poincaré disc?

A map of the hyperbolic plane, a surface where space opens up faster than on a flat sheet, squeezed into a circle. Straight lines become arcs that meet the rim at right angles, and the closer to the rim, the smaller things look: every tile here is the same size in the plane itself, and there are infinitely many before the edge.

How is the tiling drawn?

By folding, not by drawing polygons. Each pixel is mirrored into one wedge around the center, then inverted through the circle that forms a tile's edge, again and again, until it lands in a single triangle. The number of inversions says which ring of tiles it came from, which sets its color, and the triangle tells it where the lines and pulses go.

What do 3-8 and 4-6 mean?

They're the tilings' names in Schläfli notation: "3-8" is {3,8}, triangles with eight meeting at each corner, and "4-6" is {4,6}, squares with six at each corner. On a flat sheet only six triangles or four squares fit around a corner; in the hyperbolic plane more do, which is why the tiles shrink toward the rim.

What's outside the circle?

The same tiling, turned inside out: each point outside is inverted through the rim, so the center tile spreads across the corners and the tiny tiles of the rim crowd up against it. It's drawn dim and cool, so the disc stays the focus.

Can I stop the spin?

Yes: spin={0} holds the disc still while the pulses keep running, and spin={2} turns it twice as fast. speed scales everything, pulses and colors included, and paused or time holds one frame.

Will it slow my page down?

It costs about 0.3 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. Pixels near the rim take more folds, up to 24, but the tiles there are too small to draw, so they settle into an even blend.

Credits

Inspired by a Poincaré disc animation (opens in a new tab) by Shane on Shadertoy. This is a new shader written from a description of the look, not a port: the glass tiles, the network of pulses between their centers, the palette, the inverted echo outside the disc, the pointer's slide and the code are ours. The folding is the textbook construction of a hyperbolic tiling from its reflection group, and the cosine palette follows Inigo Quilez's article (MIT), with its credit line in the WGSL.

API Reference

PoincareDiscAnimationShader

data-slot="poincare-disc-animation-shader"

PropType
dashSize?number

How long the pulses of light along the network are, from 0.5 to 1.5. @default 1.

Default
1
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.

hueStep?number

How far the colour moves from one ring of tiles to the next, from 0.5 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.

speed?number

Animation speed: 1 is normal, 0 holds the frame. @default 1.

spin?number

How fast the disc turns: 1 is a turn every 50 seconds, 0 holds it still. @default 1.

Default
1
tiling?"3-8" | "4-6"

The tiling: triangles meeting eight at a corner, or squares meeting six at a corner. @default "3-8".

Type
"3-8" | "4-6"
Default
"3-8"
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