Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

ASCII Sculpture Shader for React

A turning 3D torus drawn entirely in monospace glyphs, amber on warm black, rendered on the GPU. For React with Base UI, when a CLI, a devtool or a 404 page wants a terminal-flavoured hero.

Framework

ASCII Sculpture Shader draws a large torus, off to the right of the box, turning slowly and drawn entirely in monospace glyphs: its shadows are spaces and dots, its mid-tones :-=+, and its highlights %#@, in amber phosphor on warm black, with a pink rim light along one edge. A WGSL fragment shader on WebGPU splits the box into glyph cells, ray marches the object through each cell's centre, lights it, and picks one of ten 5×6 bitmap glyphs by how bright that cell is, every frame.

Use it behind a CLI, devtool or API hero, a changelog, or a 404 page. For falling code instead of a lit form, use Matrix Shader; for a flickering grid of 0s and 1s, Binary Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a soft amber glow where the object sits 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 {
  AsciiSculptureShader,
} from "@/components/ui/ascii-sculpture-shader";
<AsciiSculptureShader className="flex min-h-[32rem] items-center p-12">
  <h1 className="max-w-sm font-mono text-5xl font-semibold text-amber-50">
    npx ship it
  </h1>
</AsciiSculptureShader>

ASCII Sculpture Shader is a container: your content sits on top of the glyphs, 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". In a wide box the object sits to the right, leaving the left for your copy; in a narrow one it's centred and scaled to fit. The page is warm black in both color schemes, so put white or light text over it.

shape picks the object: "torus", "sphere" or "cube". cellSize sets the glyph cells' width in CSS pixels; they're 1.75 times as tall, like a terminal's, and snap to whole device pixels so the glyphs stay crisp. With interactive, the object leans toward the pointer.

Demos

Green sphere

shape="sphere" turns a ball instead, where the light reads as a smooth ramp through every glyph. glyphColor="#33ff99" and rimColor="#38bdf8" make it a green-screen terminal, and cellSize={8} makes the glyphs finer.

Preview width

Interactive cube, in a card

shape="cube" turns a rounded cube, and interactive leans it toward the pointer as it moves over the card. Touch doesn't move it.

Preview width

Performance

About 0.2 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. Only the cells whose ray meets a sphere around the object march at all, so the empty page is nearly free. It keeps quality={1}: the glyphs are pixel art, and would 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 a soft amber glow on warm black where the object sits, 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 glyphs are decoration: they're hidden from assistive tech and let every click through to your content, and screen readers never read them as text. With reduced motion it draws one still frame and stops. Nothing flashes: the object turns once every 25 seconds or so, and each cell changes glyph only as the light on it changes. In the left 40% of a wide box, white text reads at about 19:1; over the object it can drop to about 2:1 on the brightest glyphs, so keep text to the left.

FAQ

How does it draw a 3D object in text?

The box is split into glyph cells. For each cell, the shader casts one ray from a camera through the cell's centre and marches it to the object's surface, using Inigo Quilez's distance functions for the torus, sphere and rounded cube. It lights the hit point with a soft key light and a rim light, and the brightness picks one of ten glyphs, from a space to @. Each pixel then reads its bit from that glyph's 5×6 bitmap.

Is it a real font? Can I copy the text?

No: the glyphs are tiny bitmaps drawn by the shader, one per cell, so there's no text to select or copy, and screen readers skip them. That also means it looks the same on every system, with no font to load.

Can I change the colors?

Yes. glyphColor sets the glyphs, rimColor the rim light along the object's right edge, and backgroundColor the page. Each takes any CSS color and follows a theme switch, so glyphColor="var(--primary)" works. Green on black makes a classic terminal; keep the background dark, so the glyphs read as light.

How big are the glyphs?

cellSize sets a cell's width, from 6 to 24 CSS pixels, and cells are 1.75 times as tall. The shader rounds each glyph pixel to whole device pixels so the glyphs stay sharp, so the real size can differ from cellSize by a pixel or two. Smaller cells give a finer, more detailed object; larger cells a chunkier, more readable one.

Will it slow my page down?

It costs about 0.2 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: the glyphs are pixel art, and the shader is cheap.

What happens in browsers without WebGPU?

The box keeps its CSS still: a soft amber glow on warm black, where the object would be. Nothing errors: the root's data-state reads unsupported.

API Reference

AsciiSculptureShader

data-slot="ascii-sculpture-shader"

PropType
backgroundColor?string

The page behind the glyphs. Any CSS colour. @default "#0c0a08".

Default
"#0c0a08"
cellSize?number

The width of one glyph cell, in CSS pixels, from 6 to 24; cells are 1.75 times as tall. @default 10.

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

glyphColor?string

The glyphs, like terminal phosphor. Any CSS colour. @default "#ffb000".

Default
"#ffb000"
intensity?number

How strongly the object is lit, from 0 (only its rim) 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.

rimColor?string

The rim light along the object's right edge. Any CSS colour. @default "#ff5f87".

Default
"#ff5f87"
shape?"torus" | "sphere" | "cube"

The turning object. @default "torus".

Type
"torus" | "sphere" | "cube"
Default
"torus"
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