Skip to main content

Design system

The foundations every component is assembled from — colour, type, space, radius, elevation, hit targets — and the decisions behind them. Each specimen below is drawn in the live tokens, so it moves with the page. The values themselves are on Tokens.

Monochrome, on purpose

There is one colour ramp and it has no hue. accent is near-black in light and near-white in dark — the same neutral scale read from the other end, not a brand colour sitting on top of a grey one.

That is a constraint with a payoff. When nothing is coloured, emphasis has to be carried by weight, size, spacing and order, which are the things that survive a screenshot, a printout, a monochrome display and a reader who cannot separate red from green. It also means the system has no opinion to impose on a host application's own palette.

Status is never colour-coded. Running, over target, failed and billable are carried by an icon, a label and a weight. A red total and a green total are the same total to a screen reader.

Surfaces

bg--hg-bgThe page.
surface--hg-surfaceA card, a sheet, a field.
elevated--hg-elevatedA code block, a well — one step up from a card.
border--hg-borderA hairline.
borderStrong--hg-border-strongA hairline that has to be seen.

Ink

fg--hg-fgBody copy, and anything that must be read.
muted--hg-mutedA second line. Still passes 4.5:1.
faint--hg-faintA label on something else. Never a sentence.
accent--hg-accentThe only emphasis the system has.
accentFg--hg-accent-fgWhat sits on the accent.

OKLCH is the source of truth, with an sRGB hex beside every value as the fallback — React Native uses the hex verbatim, having no OKLCH parser. The four derived colours are mixes of the ones above, so overriding a base colour carries through instead of leaving them stranded.

Type

Eight steps, two faces. Inter sets anything that is a sentence; JetBrains Mono sets every numeral, time, duration, eyebrow and caption.

The mono is not a code style here — it is the loudest signal the system has. A duration is a figure you compare against another figure, and figures that do not line up in a column are figures you have to read twice.

10px is the floor. Nothing renders smaller, at any step, in any library.

heroclamp(40px, 12vw, 96px) · 600 · display · -0.02em

4:40

The one place the system shouts. A landing figure, nothing else.

h128px · 600 · display · -0.01em

Four hours forty

A page title.

h222px · 600 · display · -0.01em

Four hours forty

A section.

h318px · 600 · display · -0.01em

Four hours forty

A card or a row heading.

body14px · 400 · body

Four hours forty

Everything that is a sentence.

small13px · 400 · body

Four hours forty

A second line — metadata, a note, a table cell.

caption11px · 500 · mono · 0.1em

Four hours forty

An eyebrow. Mono, uppercase, tracked out.

micro10px · 500 · mono · 0.08em

Four hours forty

The floor. A label on a figure, and nothing smaller.

Space

An 8-based ramp, with 2, 4, 12, 20 and 44 filled in where 8 is too coarse. Layout uses these and nothing between them, which is what stops a screen accumulating eleven slightly different gaps.

Whitespace before boxes. The first tool for separating two things is the space between them; a border is the second and a shadow is the third, and most screens never need the third.

22px
44px
88px
1212px
1616px
2020px
2424px
3232px
4444px
6464px

gutter is the exception: clamp(16px, 4vw, 44px), so the page margin grows with the viewport rather than stepping at a breakpoint.

Radius

Six steps, from a hairline-soft sm to a fully rounded pill. A control takes the smaller end, a surface the larger — a field is sm, a card is md, a sheet is xl.

sm8px
md12px
lg16px
xl22px
2xl28px
pill100px

Elevation

Two shadows, and the rule that there are only two. They are for things that genuinely float — a menu, a sheet, a toast — never for a card in a list, which is separated by whitespace and a hairline.

smA menu or a popover
lgA sheet or a toast

React Native has no single shadow property, so the theme splits each one into shadowColor, shadowOffset, shadowOpacity and shadowRadius for iOS, plus elevation for Android.

Hit targets

Every interactive element is at least 44px on iOS and 48px on Android, padding included. React Native picks the right one per platform; the web takes 44 and the components are built so that a small button still clears it.

44 · iOS
48 · Android
A real button

Accessibility

4.5:1 for body text and 3:1 for large text and icons, in both modes. Disabled is the only state permitted to drop below that floor, because a disabled control is not something you are being asked to read.

Focus is always visible on the web, and always the accent — never the browser's blue, which would be the only hue in a monochrome system.

Every mono numeral carries a spoken label. A screen reader says "four hours forty minutes", never "four, four, zero", because a reel is twenty stacked digits of which nineteen are decorative.

Reduced motion is honoured everywhere, whatever the application asked for: the final state, with no travel. See Motion.

Next

Tokens — every value, and the name it answers to in CSS, TypeScript and Tailwind.

Components — all 23, with props, types and code samples.