IInbox

Tokens

Semantic tokens, and why there are no raw colours in a component.

There are two layers. Primitives are the raw ramps — blue/600, gray/200 — and no component ever references one. Tokens are semantic: bg/raised, text/muted, border/hairline. Every token has a light value and a dark value, which is what lets a component stay theme-agnostic.

Surfaces

Four surfaces, and the distance between them is what makes a card read as a card.

TokenClassUse
bg/canvasbg-canvasThe page, when cards need an outline to separate them
bg/subtlebg-subtleThe page, when cards should separate by tone instead
bg/raisedbg-raisedCards, menus, anything lifted off the page
bg/sunkenbg-sunkenRecesses — segmented tracks, progress rails

bg/sunken exists because a segmented control on a bg/subtle page was invisible: its track and the page were the same colour. A recess needs to be darker than every surface it can sit on, not merely different from white.

In dark mode that inverts. You cannot cut a hole below near-black, so bg/sunken is lighter than bg/canvas there. A recess is a change in luminance, not a direction.

Tints

A tint is a surface that carries hue — never a block of colour. bg/brand-tint is what a recommendation card sits on; bg/brand is what a button is made of. They are not interchangeable, and the difference is weight.

TokenLightDark
bg/brand-tint · bg/info-tintindigo/50indigo/975
bg/error-tintred/50red/975
bg/success-tintgreen/50green/975
bg/warning-tintamber/50amber/975

The dark values alias a dedicated 975 step rather than 900. This is the system's most expensive past mistake: the dark tints originally aliased the saturated 700/900 primitives, so every "Recommended action" block and every selected table row rendered as a solid indigo slab with near-illegible text on it. A tint has to sit within about 1.2–1.4:1 of the canvas. indigo/900 (#2f3891) reads 3.4:1 — that is a fill.

red/950 is reserved for bg/diff-removed; do not repurpose it.

The colour contract

The rules above are enforced, not merely documented. npm test runs tests/tokens.test.mjs against design/tokens.json in both modes, and CI runs it before the build.

RuleThreshold
Body and secondary text on any surface≥ 4.5:1
Semantic ink on its own tint, and on neutral surfaces≥ 4.5:1
text/on-brand on every filled control, including hover and pressed≥ 4.5:1
Placeholder text≥ 3:1
Focus ring against every surface≥ 3:1
A border that reports state (border/active)≥ 3:1
A resting border (border/default, border/subtle)≥ 1.15:1
A tint against the canvas≤ 2:1
Dark elevation ladder — canvas → subtle → raised → selectedstrictly increasing
Adjacent layers that stacknever equal

Writing these down found seven real defects that had shipped: text/error and text/warning both missed AA on their own tints in light mode, dark-mode bg/brand-hover put white text at 3.9:1, and dark text/placeholder sat at 2.86:1 on a raised card. Every one had been reviewed by eye and passed.

The last two rules are not accessibility rules — they are the two bugs a human reads straight past. An active tab whose pill and track were both bg/raised had 1.00:1 against what it sat on and was simply not there.

Ink

TokenClass
text/primarytext-fg
text/secondarytext-fg-muted
text/placeholdertext-fg-placeholder
text/brandtext-fg-brand
text/success · text/errortext-fg-success · text-fg-error

Type

Type is a set of utilities rather than a set of props, so a heading is t-heading and not <Text size="xl" weight="600">.

NeutralIn reviewApprovedPendingRejected
InfrastructureDesignComplianceProcurement

What colour is allowed to mean

Green and red mean gain and loss. Brand blue means you can act on this. Identity hues — the tag/* family — separate one category from another and carry no judgement.

This is why a falling number is sometimes green: a sales cycle shortening from 21 days to 19 is a down arrow with positive sentiment. Direction and sentiment are separate props on <Delta> precisely because they disagree often enough to matter.

+12.4%-0.5%-6.2%0.0%