Skip to content

Draw your own

Draw isometric scenes of your own with the stage, shapes, exploded stacks and illustration kit that every ready-made scene is built from.

The ready-made scenes are drawn with a small kit you can draw with too. Use it when a screen explains a thing: what a device is made of, where a system's parts sit, what an empty warehouse will hold. It draws in isometric, the view of a technical drawing, so parts line up and read at a glance, and every color comes from the theme, so a drawing looks at home in any preset and either scheme. Don't use it for decoration alone. A drawing earns its space by saying something the copy beside it can't.

Four blocks draw with the kit directly, and each is a worked example:

  • First source: a floor with a ghost bay, and the chosen source waiting to land in it.
  • Tiered capacity: each region's use as a stack of tiers on a plinth.
  • Exploded view: an exploded view whose layers are named on the rail beside it.
  • Setup picker: option cards whose drawings frame alike, so their floors sit on one line.

The builder

To draw a scene without writing it, use the illustration builder (experimental). Drag the kit's parts onto the shared 8 × 8 floor: boxes and tiles, cylinders, plinths, prisms with a preset outline, and turned solids. Move them in half units and size them by their handles, or type the numbers. Light the part the scene is about, and give parts a lift for when the scene comes apart and a float. Preview shows it assembling and coming apart, in any preset, light or dark, and as reduced motion draws it.

Then take it home. Copy code writes a component the way the scenes below are written: the stage and the kit's shapes, imported from where npx @youj/cli add illustration-kit iso-shapes copies them. It draws exactly what the builder showed. The Export panel also downloads the frame as SVG or PNG and copies the scene as JSON. The scene lives in the page's address as you work, so a link opens it again, and the builder keeps your last scene in this browser. Everything works from the keyboard: the layers list chooses parts, the arrows move them on the canvas, and the keyboard button lists the shortcuts.

The builder makes new scenes. The ready-made ones are code, not data, so they open in the explorer rather than in the builder.

What the drawings share

  • Grid units in, pixels out. You place shapes on a grid (x to the lower right, y to the lower left, z up), and one function, project, turns a grid point into a point on screen. Faces, edges, callout anchors and the frame all come from it, so they agree to the pixel.
  • The theme's light. Every face is mixed from your tokens: the top face lightest, the left a step down, the right the darkest, in both schemes. The accent is kept for the part that matters.
  • One scene, one clock. Shapes assemble, part and float on motion the scene shares, from the motion tokens, so the Speed control and the theme's personality reach them.
  • Hairlines stay hairlines. A drawing is crispest at its natural size and scales down to fit a narrow screen. Its lines stay 1 px and its labels keep their size.

Installation

The kit is @youj/illustrations, a package of its own on @youj/core: pnpm add @youj/illustrations.

app/globals.css
@import 'tailwindcss';
@import '@youj/react/styles.css';   /* or '@youj/core/styles.css' without the components */
@import '@youj/illustrations/styles.css';

Its sheet points Tailwind at the built scenes, so it generates the classes they use; it goes after the tokens, in the same file. npx @youj/cli add adds it for you.

npm i @youj/illustrations

import { IsoScene } from '@youj/illustrations'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Import everything below from @youj/illustrations. To copy the source in instead, npx @youj/cli add <item> copies these items, each bringing what it needs:

ItemBrings
iso-sceneIsoScene, IsoPiece, the projection helpers and the face tones
iso-shapesIsoBox, IsoCylinder and IsoPlatform
iso-stackIsoStack and IsoCallout
grid-backdropsThe backdrops and IsoFloor
illustration-kitThe illustration kit: the stage, groups, turned and extruded solids, marks and links. Copy-in only: the package doesn't export it.

Examples

The scene

IsoScene is one SVG that holds a drawing. Put shapes inside it: boxes, cylinders and plinths, exploded stacks, a floor grid. The scene decides where they land on screen and in what order they're painted, and gives them one set of motion.

import { IsoBox, IsoFloor, IsoPlatform, IsoScene } from '@youj/illustrations'

export function EmptyBay() {
  return (
    <IsoScene title="An empty bay on a plinth" assemble>
      <IsoFloor w={8} d={6} kind="dots" />
      <IsoPlatform x={0.5} y={0.5} w={7} d={5} grid={1}>
        <IsoBox x={3} y={2} z={0.7} w={2} d={2} h={0} variant="ghost" />
      </IsoPlatform>
      <IsoBox x={1} y={1} z={0.7} w={1.5} d={1.5} h={1.2} tone="accent" />
    </IsoScene>
  )
}

Shapes are placed in grid units: x runs to the lower right, y to the lower left and z straight up, and (0, 0, 0) is the far corner of the floor. A box's x, y and z are its corner nearest the origin, and w, d and h its size along each axis.

Framing

The scene frames what's in it. Each shape reports its extent on screen, including where it reaches when it's apart (a stack at full separation, a shape's lift, the top of a bob), and the scene's viewBox is the union of those, plus pad (one number, or [top, right, bottom, left], in user units). An exploded stack is never clipped, and the drawing stays centered in its container whatever it holds.

Scenes that must line up, such as a row of option cards, frame alike with size: a volume in grid units, [w, d, h], that each scene frames instead of its content, so their floors sit on one line and their shapes at one scale. Every ready-made illustration frames the same volume this way, which is why one swaps for another.

A drawing is crispest at its natural size, where one grid unit is unit CSS px (24 by default). In a narrower container it scales down to fit; with grow it also scales up to fill a wider one. Either way, its hairlines stay 1 px and its labels keep their size: the scene writes its scale to --kg-px (user units per CSS px), and the strokes and text are sized by it. Below about three quarters of its natural size, callouts drop their detail lines and keep their names.

Painter's order

SVG paints in document order, so nearer shapes must come later. The scene sorts its direct children that are shapes far to near with isoSort before it paints them, so you can write a scene in any order. Anything else keeps its place: write the floor first and it stays under everything, write an IsoCallout last and it stays on top. Shapes drawn as another shape's children (a chip on a board) are painted with their parent, after it.

Two boxes are ordered when one lies wholly behind the other along an axis. When neither does (they interpenetrate) or a rare cycle of long boxes forms, the one whose center is farther goes first.

The rail

When a shape names its parts (an exploded stack with labeled layers, or an IsoCallout), the scene keeps a rail beside the drawing, rail CSS px wide (168 by default) at every scale, at the end of the line: on the right, or on the left on an RTL page. Leaders run from the parts to the rail's edge, and the labels stand in it, so a label never sits on the geometry. Each label has a knockout halo in the surface color; set --kg-halo on the scene when it sits on another color. rail={false} leaves the rail out.

Assemble, part and float

  • Assemble. With assemble, shapes drop in from above the first time the scene scrolls into view, far ones first, a stack's layers one after another. Each lands on the settle spring, the arrival moment the motion tokens keep a little overshoot for, and fades in on the enter curve.
  • Separation. explode is one value from 0 (together) to 1 (apart) that stacks and any shape's lift follow. Give it a number, a boolean, or "hover", which parts the drawing while a mouse pointer is over it. It moves on the smooth spring, so changing it mid-way turns the parts round from where they are, with no jump.
  • Float. Shapes with float bob gently on one clock the scene runs only while one is mounted and the scene is on screen, each a little out of step with the next.

Reference

Propssize, unit, pad… (126)

Props

IsoScene

IsoScene props
PropType
childrenReactNode–
sizeIsoSize

Frame this volume, in grid units: [w (x), d (y), h (z)], instead of fitting the shapes. For scenes that must frame alike (a row of option cards).

–
unitnumber

User units per grid unit: the drawing's natural size, in CSS px.

default 24

24
padScenePad

Room round the drawing, in user units: one number, or [top, right, bottom, left].

default 16

16
railnumber | false

Width of the callout rail beside the drawing, CSS px, when a shape names its parts; false for none.

default 168

168
titlestring

Names the drawing for assistive tech; without it the scene is decorative (aria-hidden).

–
descriptionstring

A longer description, read after the title.

–
assembleboolean

Shapes drop in, far ones first, the first time the scene comes into view.

default false

false
explodenumber | boolean | 'hover'

Separation for IsoStack layers and shapes with lift: a number from 0 to 1, a boolean, or 'hover' (apart while the pointer is over the scene).

default 0

0
growboolean

Scale up past the natural size to fill a wider box.

default false

false
colorsreadonly string[] | 'theme'

Colour for shapes that carry a hue: 'theme' for the theme's pair (derived from the accent), or any CSS colours for hue 1, 2 and 3 in order (an empty entry takes the theme's). Default off: gray, with the accent the only colour.

–
classNamestring–
styleCSSProperties–
Other props pass through: extends SVGProps<SVGSVGElement>.

IsoPiece

IsoPiece props
PropType
childrenRequiredReactNode–
liftnumber

How far it rises at full separation, in grid units along z (IsoStack sets this per layer).

default 0

0
floatboolean | number

Bob gently: true, or the height of the bob in grid units.

default false

false
delaynumber

Extra delay in the assemble, in stagger steps (a stack's layers land one after another).

default 0

0
classNamestring–
styleCSSProperties–

IsoBox

IsoBoxProps props
PropType
wRequirednumber

Size along x, y and z, in grid units. h 0 draws only the top: a tile.

–
dRequirednumber–
hRequirednumber–
xRequirednumber

The near-origin corner, in grid units.

–
yRequirednumber–
znumber

Height of its base above the floor.

default 0

0
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
colorstring

Any CSS color for the faces (token references such as var(--color-chart-2) stay in step with the theme); it wins over tone.

–
hueIsoHue
123

One of the scene's colours: the faces take it when the scene has colors, and show tone otherwise.

–
variantIsoVariant
'solid''wire''ghost'
–
hiddenboolean

Draw the hidden edges, dashed. Default: true for a wireframe or a ghost.

–
floatboolean | number

Bob gently: true, or the bob's height in grid units.

–
liftnumber

Rise this far (grid units) at the scene's full separation.

–
childrenReactNode

Drawn on the top face, moving with the shape: tiles, cylinders, marks.

–
classNamestring–
styleCSSProperties–

IsoCylinder

IsoCylinderProps props
PropType
rRequirednumber

Radius in grid units; the footprint is 2r on a side.

–
hRequirednumber

Height in grid units; 0 draws a disc.

–
xRequirednumber

The near-origin corner, in grid units.

–
yRequirednumber–
znumber

Height of its base above the floor.

default 0

0
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
colorstring

Any CSS color for the faces (token references such as var(--color-chart-2) stay in step with the theme); it wins over tone.

–
hueIsoHue
123

One of the scene's colours: the faces take it when the scene has colors, and show tone otherwise.

–
variantIsoVariant
'solid''wire''ghost'
–
hiddenboolean

Draw the hidden edges, dashed. Default: true for a wireframe or a ghost.

–
floatboolean | number

Bob gently: true, or the bob's height in grid units.

–
liftnumber

Rise this far (grid units) at the scene's full separation.

–
childrenReactNode

Drawn on the top face, moving with the shape: tiles, cylinders, marks.

–
classNamestring–
styleCSSProperties–

IsoPlatform

IsoPlatformProps props
PropType
wRequirednumber

Footprint along x and y, grid units. A round plinth takes the smaller as its diameter.

–
dRequirednumber–
hnumber

The base slab's height.

default 0.5

0.5
shape'box' | 'round'–
insetnumber

How far the plate sits in from the base's edge.

default 0.35

0.35
lipnumber

The plate's height; 0 leaves a single slab.

default 0.2

0.2
gridnumber

Engrave the plate with a grid at this spacing (grid units), square plinths only. Default none.

–
childrenReactNode

Drawn on the plate's top face.

–
floatboolean | number

Bob gently: true, or the bob's height in grid units.

–
classNamestring–
colorstring

Any CSS color for the faces (token references such as var(--color-chart-2) stay in step with the theme); it wins over tone.

–
styleCSSProperties–
xRequirednumber

The near-origin corner, in grid units.

–
yRequirednumber–
znumber

Height of its base above the floor.

default 0

0
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
hueIsoHue
123

One of the scene's colours: the faces take it when the scene has colors, and show tone otherwise.

–
variantIsoVariant
'solid''wire''ghost'
–
hiddenboolean

Draw the hidden edges, dashed. Default: true for a wireframe or a ghost.

–
liftnumber

Rise this far (grid units) at the scene's full separation.

–

IsoStack

IsoStackProps props
PropType
xRequirednumber–
yRequirednumber–
znumber–
wRequirednumber

Footprint along x and y, grid units. Disks take the smaller as their diameter.

–
dRequirednumber–
layersRequiredreadonly IsoStackLayer[]

Bottom to top.

–
shape"box" | "disk"–
gapnumber

The distance each layer adds at full separation, grid units.

default 1.4

1.4
axis"x" | "y" | "z"

The direction the layers part in. Callouts are drawn for z.

default 'z'

'z'
variantIsoVariant
'solid''wire''ghost'

The layers' variant when they don't set one.

default 'solid'

'solid'
dimboolean

Dim the other layers while one is lit.

default false

false
classNamestring–

Stack layer

IsoStackLayer props
PropType
idRequiredstring–
hRequirednumber

Height in grid units.

–
insetnumber

Brings the footprint in on every side (a board inside its case), grid units.

default 0

0
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
colorstring–
hueIsoHue
123

One of the scene's colours (see IsoBox's hue).

–
variantIsoVariant
'solid''wire''ghost'
–
labelstring

The callout's label, a mono capital eyebrow, and a quieter line under it.

–
detailstring–
litboolean

Emphasize this layer: the accent, a solid leader.

–
content(top: { x: number; y: number; z: number; w: number; d: number; }) => ReactNode

Drawn on the layer's top face, moving with it: chips, vents, a port. Gets the layer's top footprint in grid units.

–

IsoCallout

IsoCalloutProps props
PropType
atRequiredreadonly [x: number, y: number, z: number]

The point it marks, in grid units.

–
labelRequiredstring–
detailstring–
litboolean–

Group

GroupProps props
PropType
childrenRequiredReactNode–
floatboolean | number

Bob as one object: true, or the bob's height in grid units.

–
liftnumber

Rise this far (grid units) at the scene's full separation.

–
delaynumber

Extra delay in the assemble, in stagger steps.

–

Lathe

LatheProps props
PropType
cxRequirednumber

The center of its footprint, grid units. Laid down, where its axis starts.

–
cyRequirednumber–
znumber

Height of its base. Laid down, the height of its axis.

default 0

0
profileRequiredreadonly (readonly [r: number, h: number])[]

Its outline, bottom up: [radius, height above the base] at each step, heights rising. Laid down, the heights run along the axis.

–
axisAxis
'x''y''z'

The axis it's turned about: 'z' stands it up (the default); 'x' or 'y' lays it down along +x or +y, its end facing you.

–
convexboolean

Outline it as the hull of its rings: exact for a convex solid (a globe, a dome, a cup), and always so when it's laid down.

–
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
hueIsoHue
123

One of the scene's colours (see IsoBox's hue).

–
floatboolean | number–
liftnumber–
childrenReactNode–

Prism

PrismProps props
PropType
planeRequiredPlane
'top''left''right'

The plane its profile lies in. It's extruded toward you along the plane's normal: up from the top plane, +y from the left, +x from the right.

–
atRequiredreadonly [number, number, number]

The plane's origin, grid units: the back of the solid.

–
pointsRequiredreadonly PlanePoint[]

Its profile, in plane units, as a Mark's points. Curves are fine: fine steps shade smoothly, and only corners and the silhouette get a line.

–
depthRequirednumber

How far it's extruded, grid units.

–
toneIsoTone
'neutral''raised''sunken''accent''success''warning''danger'
–
hueIsoHue
123

One of the scene's colours (see IsoBox's hue).

–
floatboolean | number–
liftnumber–
childrenReactNode

Drawn on its front face, moving with it: marks on the plane depth in front of at.

–

Mark

MarkProps props
PropType
planeRequiredPlane
'top''left''right'
–
atRequiredreadonly [number, number, number]

The plane's origin, in grid units.

–
pointsRequiredreadonly PlanePoint[]

The outline, in plane units.

–
fillstring–
strokestring–
dashedboolean–
closedboolean

Join the last point to the first. Default: true when filled.

–
weightnumber

Stroke width in CSS px at any scale. Default 1, a hairline.

–
opacitynumber–
LinkProps props
PropType
pointsRequiredreadonly (readonly [number, number, number])[]

The path's corners, in grid units.

–
litboolean–
dashedboolean–
MotionAssemble, assemble fade, separation… · reduced motion

Motion

Motion tokens
MomentTokens
Assembleeach shape drops from 3 grid units above, far ones first; a stack’s layers land bottom first
spring.settlestagger.loose

0.45s, bounce 0.3 · 72ms step

0.45s, bounce 0.372ms step
Assemble fadeopacity, with the drop
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Separationexplode, "hover" and lift, translate only; a stack’s layers by their index × gap
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Calloutsopacity, from 30% to 75% of the separation
no token

follows the separation

follows the separation
Floata sine bob, out of step with its neighbors, paused offscreen
no token

one bob per 10 × deliberate

one bob per 10 × deliberate

Reduced motion

The final state, drawn once: nothing drops, nothing bobs, and the separation jumps to its target, so a drawing that's apart is shown apart, with its callouts.

Reduced motion

The scene draws its final state and holds it. Shapes that would assemble are already in place, a scene set apart is shown apart with its callouts, one set together is drawn together, and nothing floats or fades. Switch on Reduced motion in the previews above to compare.

AccessibilityARIA, focus

Accessibility

  • A scene is decorative by default: the SVG is aria-hidden and not focusable. Keep what it shows in the text around it.
  • When the drawing carries meaning the text doesn't, give it a title (and a description for more). It becomes role="img", named by its <title> and described by its <desc>. For a stack, a title that lists the parts, as the demo's does.
  • Hover to part is a flourish, not a control: it answers a mouse pointer only. Put anything a reader needs behind a real control, as the stack demo's toggle does.
  • Labels and callouts are real SVG text in the theme's inks, read with the scene when it's a named image. They keep their CSS size when the drawing scales down; keep them short, a name and a detail of a few words.
  • Don't make color the only difference. A ghost is dashed as well as pale, a lit layer's leader is solid where the others are dashed, and a lit part should also be named in the text or a callout.