Relief
Generated isometric illustration, with extruded outlines stacked and exploded and colored by role.
Relief builds isometric technical illustrations from data rather than from artwork. A shape is extruded from a vector outline, layers stack into a block, blocks are placed into an assembly, and the whole thing comes apart along authored vectors. Nothing is drawn by hand and nothing is imported, so the illustration is correct at every angle, follows the theme, and stays right when the source changes.
The argument for generating it is the same one that applies to any hand-drawn depth: a rounded box faked with a top face, two side faces, and corner slivers is correct at exactly one viewing angle and stale the moment anything moves.
The model
Three tiers, each nesting inside the next.
| Tier | What it is |
|---|---|
| Layer | One extruded outline, with a source, a depth, an explode offset, and a color role. |
| Block | A group of layers with its own internal explode. |
| Assembly | Blocks placed in space, with an explode of its own that fans whole blocks apart while their internals stay sealed. |
The two explode axes are independent on purpose: layers stack, but blocks need to fan sideways and rearrange.
Three styles, one geometry
outline is the house style, a flat face with depth carried by line work, which is what
hand-drawn isometric art was imitating in the first place. solid shades the extrusion, and
wire draws the construction with nothing filled in. All three read the same geometry.
Color follows the theme
A layer names a role, never a value. color: "surfaceRaised" resolves against whichever
look and color scheme the scene is sitting in, so one assembly repaints across every theme
without a second definition. Fills take surface roles only, and brand color lives on edges,
connectors, and glow, never as a field.
Color can climb a ladder, but only on line work. Pass edgeLadder and a layer picks its
hairline role by the same value the geometry encodes, so brand color arrives at an edge at
the loud end of a reading and never as a field. Fills stay on surface roles no matter what.
The one thing that stands outside the theme is a mark. A layer may carry fill, a CSS
hex, for a logo whose color is the brand rather than the look; its hairline takes the
same color unless edge names a role.
Materials cannot read a CSS custom property, so roles resolve at render through a probe element inside the scene's own container. That scoping is what makes two differently themed scenes on one page work with no extra machinery.
Usage
import { ReliefScene } from '@/components/relief/scene';
<ReliefScene
assembly={{
id: 'slab',
blocks: [{ id: 'stack', layers: [
{ id: 'base', source: { kind: 'roundedRect', width: 320, height: 200, radius: 20 },
depth: 12, color: 'surfaceSunken' },
{ id: 'top', source: { kind: 'roundedRect', width: 280, height: 160, radius: 16 },
at: { z: 12 }, exploded: { z: 90 }, depth: 12, color: 'surfaceRaised' },
] }],
placements: [{ id: 'only', blockId: 'stack' }],
}}
explode={{ assembly: 0.5 }}
label="A two-layer slab, half exploded"
/>Accessibility
- The canvas is
aria-hiddenunless you passlabel, which promotes it torole="img". Decorative geometry should stay hidden; an illustration carrying meaning needs the label. - Relief hangs no labels on its geometry. Copy belongs to the stage as one subtext line per beat, which doubles as that beat's accessible description.
- Nothing in the scene is focusable or interactive, so there is no keyboard surface to trap.
- Explode is a value you set, not an animation that runs on its own.
Install
shadcn add @gesso/relief