colour
Colour on our screens always means something. The canvas is dark, text comes in four steps of grey, there is one blue for actions, and there are five colours for states. Colour says what something is. It never decorates, and it never says anything on its own.
canvas
foreground
brand
signal
controls
semantic state
the palette
Every colour is a design token: a named value, stored once. The components use the name, never the colour's code, so a colour can change in one place and every screen follows. The swatches above are drawn from the same tokens, so they can't drift apart from the components.
| group | what it is for |
|---|---|
| Canvas | The page, recessed areas, panels, hover, a selected row, and the thin lines between things |
| Text | Four greys: titles and values, body text, labels and details, and hints, from the strongest to the faintest |
| Brand | The blue of the chevron, of links and of the emphasized action; a cyan for the cursor and the active stroke |
| Signal | The raw green, amber and red. The components don't use them for status; they use the states below |
| Controls | The edge that makes a control visible, and the keyboard focus outline |
| States | The colour of a status, a message or a toast: one of five meanings |
semantic state
Statuses, messages and highlights take their colour from a state, never from the name of a colour. Each state has a colour for its text and symbol, a faint tint and a border. Every component that shows a state also gives it a symbol of its own shape, so the state never rests on colour alone.
| state | means | for example | never for |
|---|---|---|---|
| neutral | A plain fact, with no judgement | “missing”, “queued”, “not searched yet” | Something that needs attention |
| information | Under way, or worth knowing; nothing to fix | “on its way”, “syncing”, “asking again” | Decoration, or “this is blue” |
| success | Done, and good | “held”, “saved”, “healthy” | Anything with a problem in it, however small |
| warning: warning | It works, but not fully; someone should look | “connection firewalled”, “degraded”, “can't be checked” | A failure (that is an error), or emphasis |
| error: error | Failed or blocked; it needs action | “needs attention”, “unreachable”, “couldn't load” | An action that destroys something (that is the danger button) |
The words decide the state, not the other way round. Write the status in the object's own words first, then ask which of the five meanings it has. A domain's whole vocabulary lives in one table; status vocabulary shows how.
using colour
- Text has four weights. The brightest grey for titles and values, the next for body text, a muted grey for labels and details, and a dim grey for hints and the least important line. To make something quieter, make it shorter rather than dimmer.
- On a tinted background, use the muted grey or brighter. The dim grey isn't clear enough on two of the tints.
- Blue is the action colour. It marks the chevron, links, the emphasized button, the selected tab and the bar of a selected row. Information uses the same blue, so make information read as information, with words and a symbol, and not as a link.
- Status goes through a component. The status, the banner, the toast, the filter tabs' counts and the data table's highlighted rows take a state, never a colour. The badge still has colour tones: use it for labels and counts, not for status.
- Never colour alone. A state also has a symbol and words, a selected row also has a ticked checkbox, and a field with an error also shows the error in words.
contrast
Text has to stand out enough from what is behind it to be read, by everyone, including people with low vision. That difference is the contrast ratio. Every pair below is worked out from the tokens by the same code that the tests run, and a pair below its minimum fails the build. So a change to a token can't quietly break this table.
98 pairs checked, 98 pass. The tightest text pair is --fg-dim on --surface-2 at 4.63:1.
| text token | --bg | --bg-2 | --surface | --surface-2 | --surface-selected |
|---|---|---|---|---|---|
| --fg | 15.34, passes | 15.16, passes | 14.53, passes | 13.80, passes | 13.86, passes |
| --fg-2 | 12.41, passes | 12.26, passes | 11.75, passes | 11.16, passes | 11.21, passes |
| --fg-muted | 9.52, passes | 9.40, passes | 9.01, passes | 8.56, passes | 8.60, passes |
| --fg-dim | 5.15, passes | 5.09, passes | 4.87, passes | 4.63, passes | 4.65, passes |
| --cloud-blue | 7.58, passes | 7.49, passes | 7.18, passes | 6.82, passes | 6.85, passes |
| --cloud-cyan | 6.70, passes | 6.62, passes | 6.34, passes | 6.02, passes | 6.05, passes |
| --state-neutral | 9.52, passes | 9.40, passes | 9.01, passes | 8.56, passes | 8.60, passes |
| --state-information | 7.58, passes | 7.49, passes | 7.18, passes | 6.82, passes | 6.85, passes |
| --state-success | 5.68, passes | 5.61, passes | 5.37, passes | 5.11, passes | 5.13, passes |
| --state-warning | 10.21, passes | 10.09, passes | 9.66, passes | 9.18, passes | 9.22, passes |
| --state-error | 5.71, passes | 5.65, passes | 5.41, passes | 5.14, passes | 5.16, passes |
The other rules cover a state's text on its own tint, body text on every tint, the edges that make a control visible (WCAG 1.4.11 asks for 3 to 1), and text on a filled control. A label or an icon on a filled colour, blue, green, red or amber, is always the darkest background colour: white on these colours is too faint to read, especially from across a room.
| foreground | on | sample | ratio |
|---|---|---|---|
| --state-neutral | --state-neutral-bg | sample text | 8.56, passes |
| --state-information | --state-information-bg | sample text | 6.51, passes |
| --state-success | --state-success-bg | sample text | 5.05, passes |
| --state-warning | --state-warning-bg | sample text | 8.56, passes |
| --state-error | --state-error-bg | sample text | 5.14, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --fg | --state-neutral-bg | sample text | 13.80, passes |
| --fg-2 | --state-neutral-bg | sample text | 11.16, passes |
| --fg-muted | --state-neutral-bg | sample text | 8.56, passes |
| --fg | --state-information-bg | sample text | 13.17, passes |
| --fg-2 | --state-information-bg | sample text | 10.65, passes |
| --fg-muted | --state-information-bg | sample text | 8.17, passes |
| --fg | --state-success-bg | sample text | 13.64, passes |
| --fg-2 | --state-success-bg | sample text | 11.03, passes |
| --fg-muted | --state-success-bg | sample text | 8.46, passes |
| --fg | --state-warning-bg | sample text | 12.87, passes |
| --fg-2 | --state-warning-bg | sample text | 10.41, passes |
| --fg-muted | --state-warning-bg | sample text | 7.98, passes |
| --fg | --state-error-bg | sample text | 13.81, passes |
| --fg-2 | --state-error-bg | sample text | 11.17, passes |
| --fg-muted | --state-error-bg | sample text | 8.57, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --border-control | --bg | 3.62, passes | |
| --border-control | --bg-2 | 3.58, passes | |
| --border-control | --surface | 3.43, passes | |
| --border-control | --surface-2 | 3.26, passes | |
| --border-control | --surface-selected | 3.27, passes | |
| --focus-ring | --bg | 6.70, passes | |
| --focus-ring | --bg-2 | 6.62, passes | |
| --focus-ring | --surface | 6.34, passes | |
| --focus-ring | --surface-2 | 6.02, passes | |
| --focus-ring | --surface-selected | 6.05, passes | |
| --cloud-blue | --bg | 7.58, passes | |
| --cloud-blue | --bg-2 | 7.49, passes | |
| --cloud-blue | --surface | 7.18, passes | |
| --cloud-blue | --surface-2 | 6.82, passes | |
| --cloud-blue | --surface-selected | 6.85, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --fg | --surface-focus | sample text | 10.41, passes |
| --fg-2 | --surface-focus | sample text | 8.42, passes |
| --fg-muted | --surface-focus | sample text | 6.46, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --focus-ring | --surface-focus | sample text | 4.54, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --bg | --cloud-blue | sample text | 7.58, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --bg | --signal-amber | sample text | 10.21, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --bg | --state-success | sample text | 5.68, passes |
| foreground | on | sample | ratio |
|---|---|---|---|
| --bg | --state-error | sample text | 5.71, passes |
| tokens | why |
|---|---|
| --border, --border-2 | Decorative hairlines between regions. They never alone identify a control: inputs, selects and checkboxes use border-control. |
| --state-*-border | Chip outlines. A chip is identified by its text and its tint, both of which are checked. |
| --signal-green, --signal-amber, --signal-red | Checked through their aliases state-success, state-warning and state-error, which share their values. |
themes
The system is dark only, and it tells the browser so, which makes the browser's own controls, scrollbars and form fields dark too. A light theme and a high-contrast theme are planned, but they aren't here yet: there is no theme switch, and nothing follows a person's light or dark preference, or forced colours, today. When forced colours are on, the browser's colours replace the palette.
do and don't
related
- Status vocabulary: giving a domain's states one of the five meanings.
- Accessibility: the baseline that the contrast rules are part of.
- Brand: the chevron and its one colour.
- Tokens: the colours in CSS, TypeScript and Tailwind.
specs
| group | tokens |
|---|---|
| Canvas | --bg, --bg-2, --surface, --surface-2, --surface-selected, --border, --border-2 |
| Foreground | --fg (titles and values), --fg-2 (body text), --fg-muted (labels and meta), --fg-dim (hints and helpers) |
| Brand | --cloud-blue (the > chevron, links, the primary action), --cloud-cyan (the cursor, the active stroke) |
| Signal | --signal-green, --signal-amber, --signal-red |
| Controls | --border-control (a control's boundary, 3:1), --focus-ring (the keyboard focus outline) |
| Semantic state | --state-*, --state-*-bg, --state-*-border for neutral, information, success, warning and error |
- Source. Every colour is a token in
tokens/tokens.json. Components read the CSS custom properties (var(--fg-2)), never a hex value. The swatches are painted with the custom properties and labelled from the typed tokens, both generated from that file. - On a tint. Use
--fg-mutedor brighter.--fg-dimis outside the policy there: 4.32:1 on--state-warning-bgand 4.42:1 on--state-information-bg. - Blue.
--cloud-bluemarks the chevron, links, the primary button, the selected tab and a selected row's rule. - Status. The status, the banner, the toast, the filter tabs' counts and the data table's
rowHighlighttake astate; the badge takes atone. - Contrast. Computed by
tokens/contrast.mjs, the same function the token test runs (tokens/contrast.test.ts, part ofnpm testand of CI) and the token build warns with. - Themes. The tokens declare
color-scheme: dark. Nothing responds toprefers-color-schemeorforced-colorstoday.