Skip to content

System · v2.0

The rules behind litt.design, this gallery and the player that runs it. Tokens live in src/design-system/tokens.css, primitives in src/design-system/primitives, the reasoning in DESIGN.md.

Principles

  1. 1An object, not a page

    The home page is a player you could hold: a body in a machined frame, wells pressed into it, keys that sink 2px. A study is a part of the same instrument, never a card on a screen.

  2. 2An instrument's vocabulary

    Takes, the tape, transport, slate, detents, scenes. Name things the way a field recorder would, and the interface explains itself.

  3. 3It runs by itself, and rewards the hand

    Everything moves in its first four seconds, untouched: a rehearsal, a recall, a ghost typing. A hand takes over at once, and the motor lets go.

  4. 4It makes sound

    Every press clicks, synthesized as it plays. What a study does by itself follows the player's tape; what the viewer does always sounds.

  5. 5Restraint, used as a signal

    Monochrome, plus red for REC and the playhead, orange for HOLD and one blue on the screen. Figures are tabular. The work is the loudest thing here.

  6. 6Physics carry the feel

    Springs that keep their velocity, motors against hands, detents, over-centre snaps, tape that winds by the square root and needles with VU ballistics. Where a mechanism exists, model it.

  7. 7Accessibility is native

    Real roles and ARIA patterns, one tab stop per composite, focus that follows the hand, changes said in live regions. Reduced motion stops anything that loops by itself.

  8. 8Product patterns, with the player's polish

    A study is something teams ship: a command menu, a 2FA field, a toast. The hardware is the polish; the pattern is the subject. The craft speaks for itself, with no explanatory chrome.

Colour

Neutral grounds, three text greys, one accent. Every value is a token with a light and dark definition.

Light

  • canvas#fafafa
  • panel#f5f5f5
  • surface#ffffff
  • ink#0a0a0a
  • muted#707070
  • subtle#a3a3a3
  • linergb(0 0 0 / 0.08)
  • line-strongrgb(0 0 0 / 0.14)
  • accent#384ecb
  • accent-ink#ffffff
  • accent-strong#384ecb
  • danger#d93036

Dark

  • canvas#0a0a0a
  • panel#111111
  • surface#171717
  • ink#ededed
  • muted#8f8f8f
  • subtle#5c5c5c
  • linergb(255 255 255 / 0.08)
  • line-strongrgb(255 255 255 / 0.15)
  • accent#384ecb
  • accent-ink#ffffff
  • accent-strong#8a9af2
  • danger#ff6166
canvas
Page ground
panel
Recessed areas, stages, code
surface
Raised objects: cards, menus, toasts
ink
Primary text and solid buttons
muted
Secondary text, meta, icons
subtle
Decoration and disabled only
line
Hairline borders and dividers
line-strong
Hover borders, inputs
accent
Blue fill: status dots, highlights
accent-ink
Text on accent fills
accent-strong
Accent as text or stroke
danger
Destructive and negative values

Signals

Colour is never decoration. Each of these means one thing, and a light that glows is a signal; a lit segment is not.

  • --device-rec

    REC, PLAY, the playhead, a light that fires, a wrong code. The only red on the body.

  • --device-hold

    A hand on a motor (TOUCH), an edited scene, a switched-on stripe.

  • --accent

    Screens and pages, never the body: the highlight bar, the caret, a status dot.

  • --device-meter-on

    Everything else that lights: meter segments, step keys, a knob's ring.

Type

Geist Sans for everything, Geist Mono for code and figures on the page. Small sizes, tight tracking, weight 400 and 500 on the page; the hardware adds light figures and semibold lettering.

  • text-display

    32/1.15 · -0.035em

    Page titles, rarely

  • text-title

    20/1.35 · -0.018em

    Component and section titles

  • text-lead

    17/1.55 · -0.011em

    Intro paragraphs

  • text-body

    14/1.6 · -0.006em

    Default text

  • text-meta

    12/1.4 · -0.005em

    Dates, labels, captions

  • Figures that change are tabular (tabular-nums), on the screen and on the hardware, so a count never shuffles sideways.
  • The hardware has its own lettering, sized in em from its plate: engraved captions at 0.6em, semibold, capitals tracked 0.16em; key lettering at 0.8em, medium, tracked 0.03em; LCD figures in light (300) at −0.03em. See Materials.

Shape

Radii grow with object size. Elevation is a hairline ring plus a soft shadow, never a heavy drop.

  • rounded-key 2pxButtons
  • rounded-sm 6pxChips, kbd
  • rounded-md 8pxInputs
  • rounded-lg 12pxCards, previews, toasts
  • rounded-xl 16pxStages, large panels
  • shadow-smButtons, chips
  • shadow-mdCards, inputs
  • shadow-lgMenus, toasts, dialogs

On the hardware, radii are in em and nest: a plate at 1.25em, a well at 1.05em, a key or an LCD at 0.7em, the chip at 0.4em. Elevation is light, not drop: a lit top edge, a recess for anything pressed in, a 2px base under every key.

Materials

The player's hardware, rendered from its tokens. Studies are built from these parts, so each reads as part of the same instrument.

Play4/20

00M14S06F

take_04.wav

Well · LCD · meters
Window
Keys · rest, pressed
  • Off
  • On
  • Rec
  • Hold
Lights

Hold to erase1 kHz · reads 0 VU

Lettering · label, quiet
Caps in collars
Plate
--device-body--device-body-edge--device-grain
The body's bead-blasted finish: a lit top edge, soft-light noise over a vertical gradient. Every study sits on one.
Frame
--device-frame--device-frame-edge--device-body-shadow
The player's machined frame: a band a shade darker than the body (lighter, in dark), lit along its top edge.
Bezel
--device-rim--device-rim-edge
Black, round LCD cells, dot-matrix strips and a second screen, its polished edge catching the light.
Well
--device-well--device-recess
Anything pressed into the plate: the readout, the key row, a fader's slot, a collar.
Key
--device-key-face--device-key-ink--device-key-shadow--device-key-shadow-pressed
A raised key on a 2px base it sinks onto. Both shadows keep the same five layers, so they interpolate.
Cap
--device-wheel-face--device-wheel-shadow--device-wheel-shadow-pressed
Round caps and the dial: lit from above, seated in a collar cut from the well.
LCD
--device-lcd--device-lcd-edge--device-lcd-ink--device-lcd-dim
Black glass with a diagonal sheen. Light figures, a white chip for the state, dim units.
Window
--device-window--device-window-edge
Smoked glass set deeper than the LCD: the maker's window, the reels, a fine grille behind it.
Lights
--device-meter-on--device-meter-off--device-rec--device-hold
Monochrome when lit, red for REC and the playhead, orange for a hand on it. Signal colours glow; meter segments don't.
Lettering
--device-label--device-label-quiet--device-engrave--device-engrave-glyph
Tiny tracked capitals printed on the body, with a 1px highlight on the side away from the light.
Glass
--screen-glass--screen-glow
Over a screen: a sheen where the light catches it, and the backlight's bloom as it wakes.
Drawing
--device-draw-line--device-draw-ink--device-hold
A study taken apart, drawn: hairlines and lettering over the canvas, the picked layer in the hand's orange.
  • Plate, then well, then part: wells are pressed into the plate; keys, LCDs and collars sit in wells; caps sit in collars. Grain goes on the plate only, as .device-grain at --device-grain (0.3).
  • Size it all in em from one font size on the plate, so the object scales as one piece. Keys sink 2px onto their base in 75ms and come back at --duration-exit.
  • The LCD carries a white chip with a glyph for its state (a red dot that pulses for play), light figures and dim units. Signal lights glow with a shadow in their own colour; unlit ones are --device-meter-off.
<div className="@container w-full max-w-[360px]">
  <div className="relative isolate overflow-hidden rounded-[1.25em] p-[0.9em]
    text-[clamp(11px,4cqw,14px)] [background:var(--device-body)]
    shadow-[var(--device-body-edge),0_1px_2px_rgb(0_0_0/0.06),0_16px_32px_-18px_rgb(0_0_0/0.3)]">
    <div aria-hidden className="device-grain pointer-events-none absolute inset-0 -z-10 rounded-[inherit]" />
    <div className="rounded-[1.05em] bg-(--device-well) p-[0.45em] shadow-(--device-recess)">…</div>
  </div>
</div>

Motion

Curves after Emil Kowalski, durations from litt.design. Ease out for anything triggered by the user.

600ms each, so only the curve differs.

  • --ease-out

    Default for anything entering or responding to input

  • --ease-in-out

    Things moving on screen from A to B

  • --ease-drawer

    Sheets and drawers (iOS-like)

  • --ease-spring

    Small overshoot for playful returns

  • --duration-exit 150ms

    Leaving, closing, hover-out

  • --duration-enter 210ms

    Appearing, opening, hover-in

  • --duration-move 400ms

    Layout and position changes

  • --stagger 50ms

    Delay between siblings entering together; cap at 8 items

Springs

Use a spring when motion follows the pointer or can be interrupted: magnetic elements, indicators, anything dragged. Values go straight to the DOM through createSpring, so nothing re-renders.

Press twice quickly: springs reverse without a jump.

  • springs.snappy

    k 520 · c 40

  • springs.gentle

    k 170 · c 22

  • springs.bouncy

    k 260 · c 12

Physics

Where a study stands in for a mechanism, it models the mechanism: a small integrator or a spring with real numbers, not a duration and a curve.

  • Motor and hand

    Knob, Fader

    Values set from outside travel on a spring (the Knob on springs.gentle in degrees, the Fader on k 240 · c 30 in permille); the hand's land at once. A hand on the cap stops the motor; let go, and it takes the cap to wherever the value moved on to.

  • Detents

    Knob, Fader, the dial, Tape Reels

    A click for every detent the spring passes, pitched with the value. The dial clicks every 15°, a reel every 30°. The Fader's unity detent catches within 1.2% of the range and lets go 3.5% clear.

  • Over-centre

    Switch

    The cap lags the finger toward the centre (3% breakout, the snap at 72% of finger travel), then a spring of 40 preload plus 260 at its peak throws it to a hard stop (k 16000). Release projects the cap 150 ms ahead at the finger's speed: a flick decides by velocity.

  • Tape

    Tape Reels

    A pack's radius grows with the square root of the tape on it, and each reel's angle comes out in closed form, θ = 2(r − r_hub) / k. Progress eases in on k 140 · c 24, critically damped: the reels have mass.

  • VU ballistics

    VU Meter

    99% of a steady tone in about 300 ms with about 1.5% overshoot (k 166 · c 20.2). The peak light holds 600 ms past +3 VU. 0 VU is the slate tone, as measured leaving the bus.

  • Counter drums

    Number Ticker

    Each drum turns on its own spring (k 150 · c 21) and clicks for every figure that passes. Counting up, drums carry forward through 9 to 0, 45 ms apart from the right.

  • Stagger siblings that enter together by --stagger (50ms), capped at 8, so a long list never waits on its last item. Motors that move one after another wait longer: 120ms between a desk's knobs.
  • Agent states: animate-shimmer for a working label, animate-wave and animate-hop for pixels and dots, animate-caret while text streams. Announce phases to screen readers, not tokens.
  • Agent shapes: animate-stretch for rays, animate-trace to run a dash along a path (set pathLength={1}), animate-morph for a form changing shape, animate-assemble for tiles folding out along --fx / --fy, animate-twist to turn a layer a quarter at a time, and animate-build, which runs a shared --build value from 0 to 1 and back for children to read. Offset siblings with negative delays so a loop never starts at rest.
  • Press feedback: hardware keys sink 2px in 75ms; flat controls take active:scale-[0.97].
  • Hover in at enter speed, out at exit speed, so the UI never lags behind the pointer.
  • Never animate from scale(0). Start at 0.95 or more with opacity.
  • Respect reduced motion: transitions collapse to instant, motors jump, rehearsals don't start, autoplay media shows a still.

Primitives

The small set the gallery is built from. Import from @/design-system.

Button<Button variant="primary">
IconButton<IconButton label="Search">
motionDraftShipped Available
Badge and Dot<Badge variant="accent">
⌘Kto search
Kbd<Kbd>⌘</Kbd>

Sound

Clicks after the 2009 click wheel, a little warmer. Synthesized with Web Audio; nothing loads.

On. Press a key to hear it.

tick
One detent of the wheel. Fired up to 70 times a second, so it stays tiny.
bump
The end of a list: the same detent, duller.
press
A key going down: a low thock with a little body.
release
The key coming back up: lighter and quieter.
select
The centre button: a firm click and a two-note confirm.
back
Menu: a softer click, the confirm falling.
open
A prototype taking the screen: a click and air sweeping up.
close
Leaving it: the sweep going down.
toggle
A switch: two tiny clicks.
start
The tape rolling: a latch, then a rising two-note beep.
stop
Stop: a heavier thock, a low note winding down.
slate
The line-up tone: 1 kHz held for most of a second, which a VU meter reads as 0.
wake
The device waking, once per visit.
  • Fire and forget: play("select") from @/lib/sound. It never throws, and stays silent until the first press or key.
  • Keys sound by themselves: Button and ButtonLink press and release, ghost buttons tick. Opt out with data-sound="off".
  • Quiet and dry: one bus at a conservative level (peaks near -18 dBFS), a faint room and a compressor; 2.5 dB lower on touch screens; never more than eight voices at once. Volume moves the bus in 2 dB steps around 7, the level every sound was tuned at.
  • In time: sequencers pass { delay } to schedule a note up to 0.5 s ahead on the audio clock. The rate limit counts when a sound will be heard, and scheduled notes never take the last two voices, so a key press always clicks.
  • slate is the recorder's line-up tone: 1 kHz held for most of a second, which a VU meter reads as 0. readLevels() returns what leaves the bus, for meters.
  • Vary a repeated sound with pitch and gain (a detent at 0.97 to 1.03), never with new sounds. Mute is remembered per device and follows across tabs.

The transport rule

On the player, the running study carries the tape's transport as data-transport; hostTransport(el) reads it. Sounds a study makes by itself ask first. Sounds from the viewer's own input always play.

  • playAloud.
  • pauseRuns, but quiet until the viewer works it (a pointerdown or keydown on the study).
  • stopStays put. STOP remounts the study at its first frame; it doesn't start by itself.
  • no hostComponent pages and capture frames read play.

Studies

The bar a study meets before it goes on the player. It gets a few seconds, untouched, and a phone a small screen.

702 × 374

Capture frame

282 × 332

Smallest stage

To scale: the smallest stage a study gets (the player's floor zoom on a phone, less the stage's padding) beside the capture frame it is filmed in.
  1. 1Moves in its first four seconds

    Untouched: a rehearsal, a recall, a ghost typing, automation playing back, a self-test. Never still for more than half a second at the start. Then the hand takes over.

  2. 2Fits 282 × 332

    A root @container w-full max-w-[…], a cqw-clamped font size on its child, everything else in em. Size to the container, never the viewport. Touch targets 24px or more.

  3. 3Built from the materials, and marks them

    A plate of the body's finish with wells, keys, caps, an LCD and lights. Never a second player, never a flat shadow-md card. Both themes look intentional. Each part carries data-part, so its page can take it apart.

  4. 4Sounds through play()

    Existing sounds only, varied with pitch and gain. What it plays by itself follows the transport rule; what the viewer does always sounds.

  5. 5Turns without transforms

    Rotation is conic-gradient(from var(--a)) or SVG attributes, or a transform inside an overflow-hidden wrapper, so a turning part never spills its stage.

  6. 6Rounds its trigonometry

    Coordinates rendered on the server are rounded to two decimals, so the browser's Math agrees when it hydrates.

  7. 7Passes the React Compiler rules

    No ref.current during render, no synchronous setState in an effect body, no Math.random or performance.now in component-scope helpers. useEffectEvent for what effects call.

  8. 8Accessible from the start

    Native roles, one tab stop per composite, focus that follows the hand (no ring on pointerdown), live regions for changes, and reduced motion respected.

  9. 9Draws without React

    Per-frame work writes CSS variables and attributes, and springs come from createSpring. React hears about commits, not frames.

  10. 10One file

    A real, controlled component and a default Demo with a single root, importing react, @/lib/sound and @/design-system only. It never takes focus on mount.

Run npm run check:fit before pushing: it loads every component page, the player's screen and the capture frame at six widths and fails if anything spills its stage. npm run check:anatomy takes every study apart on its page and fails if anything flattens the 3D or spills.

Writing

Plain, precise, British-spelled and quiet.

  • Sentence case everywhere

    “Selected work”, not “SELECTED WORK” or “Selected Work”.

  • Labels say what happens

    “Copy command”, then “Copied”. Not “Submit” or “OK”.

  • Short and plain

    Write it the way you'd say it to someone at the next desk.

  • Parentheses for status

    “Carson (In progress)”. A blue dot comes with it.

  • British spelling, with -ize

    “Colour”, “centre”, “dialled”; but “synthesized”, “motorized”. Oxford spelling.

  • Say it once

    If the screen already says it, the LCD doesn't. Repeats are taken out, not restyled.

  • Units and real symbols

    “−3.5 dB”, “48 kHz · 24 bit”, “282 × 332”: a true minus, a times sign, a middle dot.

  • Capitals belong to the hardware

    Lettering on the body is set in capitals by CSS. The source and screen readers keep sentence case: “Hold to erase”.

  • The recorder's world, in demos

    take_04.wav, Export, Slate, Scene A. The component stays general; its demo speaks the instrument's language.