Skip to content
The Handover

DesignRootnotes

Design system context for an agent building interfaces

Why an agent drifts a visual language one plausible value at a time, and the rules that stop it.

How interfaces are built here. This is not a component catalogue — it is the set of rules an agent needs so that what it builds belongs to the same product as everything already built.

The failure this prevents

An agent asked to build a component will produce something that looks correct in isolation. Given a blue it needs, it writes a blue. Given a gap it needs, it writes a gap. Each choice is defensible on its own, review sees a reasonable diff, and the visual language drifts by a few degrees per change.

The result is not one obvious break. It is forty near-misses — six blues, five spacings that are almost the scale, three shadows nobody chose — and by the time it is visible, the fix is a project rather than an edit.

Drift accumulates below the threshold of review. That is what makes it worth a document rather than an instruction.

Tokens are the source of truth

Design values live in {{tokens_path}}. Nothing else defines them.

  • Never hardcode a value that exists as a token. Not a hex code, not a pixel spacing, not a radius — including “just this once” and including inside a one-off component.
  • If the value you need does not exist as a token, stop. Adding a token is a change to the visual language, not an implementation detail. Propose it, say what it is for and why the existing scale does not cover it, and let someone decide.
  • Prefer the nearest existing token to a new one. Almost always the design survives the rounding, and a scale with three near-identical steps is worse than a design that used the step above.

The same applies to type scale, spacing scale, radii, shadows, motion durations and z-index layers. Anything with a scale has a scale for a reason.

Every state is part of the component

A component is not done when it renders. The states that get skipped are the ones people actually hit:

  • Default, hover, focus, active, disabled — focus especially, see below.
  • Loading — including what it looks like when loading takes longer than expected, which is when the user starts wondering if it is broken.
  • Empty — the first thing a new user sees. Usually the most neglected state in any product and the one with the most influence on whether they continue.
  • Error — what it looks like when the thing fails, with recovery reachable from that state rather than requiring a reload.
  • Too much content — long names, long lists, long words that cannot wrap. Test with the longest plausible real value, not with “Test”.

If a state is genuinely not designed yet, say so rather than inventing one. An invented empty state becomes the empty state.

Accessibility is a floor, not a feature

This project holds itself to {{accessibility_standard}}. The full commitment, including what is tested and how, is a separate rootnote; what follows is what bears on building a component.

  • Contrast applies to text, to icons carrying meaning, and to the boundaries of interactive controls. Check it rather than eyeballing it — the failures cluster in exactly the places that look fine, like grey secondary text and brand colour on white.
  • Focus must be visible and must never be removed. Removing the focus outline because it looks untidy makes the product unusable by keyboard. If it looks untidy, style it.
  • Interactive targets need adequate size, with adequate spacing between adjacent ones. Adjacent small targets are the pattern that produces mis-taps.
  • Nothing conveyed by colour alone — pair it with text, an icon or a shape.
  • Respect reduced-motion preferences. Anything animating needs a still equivalent, not a shorter animation.
  • Semantic elements before ARIA. A button element beats a div with a role, every time. ARIA is for what HTML cannot express, not a way to make the wrong element behave.

Accessibility failures are the class of defect most likely to reach production, because they are invisible to the person who introduced them.

Before building a new component

  • Check whether one exists. A near-duplicate is worse than a slightly imperfect fit, because both then need maintaining and they diverge.
  • Extend before you fork. If an existing component almost fits, adding a variant is usually right; copying it and editing is usually not.
  • Compose from primitives rather than building bespoke layouts, so spacing and alignment inherit the scale instead of being retyped.
  • Name it for what it is, not what it looks like. Callout survives a redesign; YellowBox does not.

Responsive and density

State what breaks first as the viewport narrows, and design that deliberately rather than letting it be discovered. Tables, long headings, side-by-side layouts and fixed-width controls are the usual candidates.

Test at the smallest supported width with the longest real content. Both extremes at once is where the layout actually fails, and testing them individually finds neither.

The boundary with the editorial guide

This document owns the container and the behaviour: what a control looks like, how it responds, what states it has, how it is spaced.

The words inside it — button labels, error text, empty-state copy, terminology — belong to the editorial guide. When the two meet, this document sets the constraint and the editorial guide sets the style: this says a button label must fit on one line at the smallest width; the editorial guide says a button label is a verb.

If a component’s design depends on copy that does not exist yet, that is a question for whoever owns the words, not a placeholder to invent.

What needs a human

  • Adding or changing a token. That is the visual language itself.
  • Introducing a new component pattern that other things will copy.
  • Anything that changes how a familiar interaction behaves, where the cost is paid by people who already learned the old one.
  • Relaxing an accessibility rule, which is a decision with legal and ethical weight and is never a tidiness matter.

An agent may prepare any of these with a worked example and an argument. It should not merge one.