Colour

Ten families of eleven stops, a twelfth in the two surface ladders, 94 roles, and a rule that keeps them apart: nothing in the product references a primitive directly.

How colour is organised

Three layers, and a seam between the first two. Primitives name a value — twilight/600 — and are never applied in a component. Theme tokens name a job — interactive/accent — and point at a primitive; they are the only layer a component references, and the only one that changes between light and dark. That seam is what makes the dark theme possible: without it, dark mode would mean auditing every use to work out which meant “the accent” and which just wanted violet.

Primitives

The raw ramps. These carry no meaning and are not used in components — they exist to be aliased. Every family is generated in OKLCH and shares one lightness per stop, so a number means the same amount of light everywhere: any 600 carries a white label at 4.5:1 or better, any 400 carries a night/950 label, any 500 is the 3:1 stop for icons and borders, any 700 is text on its own 050, any 300 is text on its own 900. Chroma and hue are each family’s own.

There are no half steps for text or fills. The twenty-step neutral this replaced had adjacent steps 1.08 to 1.23:1 apart, and produced two text levels nobody could tell apart. The one exception is 925, the surface step, in stone and night only: no text is ever set in one surface against another, and every reference system measured places its surface levels closer than any ramp places its text stops — see Elevation and states. When a ladder runs out, separate with a border.

glow the brand — hero, gradient, one call to action per screen; never a button tone

050100200300400500600700800900950

twilight everything interactive — fills, links, focus, selection

050100200300400500600700800900950

flare the highlight fill — the tertiary tone

050100200300400500600700800900950

glacier the second highlight, and the info status

050100200300400500600700800900950

stone the neutral foundation — text, borders, the light canvas

050100200300400500600700800900925950

night the dark surface ladder

050100200300400500600700800900925950

mist soft states — the wash that hover and pressed lay over any surface, in both modes

050100200300400500600700800900950

ember danger

050100200300400500600700800900950

moss success

050100200300400500600700800900950

amber warning

050100200300400500600700800900950

One lightness per stop

The lightness is stone’s, read in OKLCH; every other family sits within a thousandth of it. Each stop’s job is measured across all ten families as the page renders, and the family shown is the one where the pair is tightest.

One lightness per stop
StopLCarriesTightest
0500.975—
1000.945—
2000.896—
3000.824text on its own 900glow 9.36AAA
4000.730a night/950 labelglow 7.51AAA
5000.625icons and borders on white, 3:1moss 3.353:1
6000.525a white labelmoss 5.09AA
7000.430text on its own 050moss 7.25AAA
8000.329—
9000.244—
9500.159—

Choosing a colour

Reach for the role, never the value. A component that needs the accent takes interactive/accent; if what it needs has no token, the answer is a new token, not a primitive that happens to look right today — a separator borrowed as a text colour reads until borders get lighter, and then the text goes with them.

One colour, one meaning. The accent means interactive, so that hue on static text tells the reader to click something that is not a control. glow is the brand and never a button tone: glow/600 and ember/600 are 1.02:1 apart, and a pink button beside a danger button would be two of the same thing. amber is warning and flare the highlight, kept far enough apart at 400 to read as two. And filled colour is emphasis: one solid accent per view, with the alternative beside it neutral — see Button.

Measure the pair that renders. Text against the surface it sits on, a border against the fill it edges, a label against its own button in every state, and an alpha over the ground it lands on. The tables below do exactly that, with the function the test suite runs; a value written by hand is a value that can rot.

Surfaces

A ladder of elevation rather than a set of colours. In light, raised and overlay are both white and the shadow does the separating; in dark, overlay steps lighter because shadow no longer reads as height, and sunken shares the canvas because the ramp ends at 950 — a well on the canvas takes a border. The dark ladder is night 950, 925, 900, ΔL .043 per step; a product that wants a neutral dark aliases the same stops of stone, and every pair holds — the tightest, border/strong on stone/800, is 3.44:1. The tail is deep on purpose: 700 to 950 sit at L .43, .33, .245, .205 and .16, so the dark canvas reads as night rather than slate. Alpha tokens are shown flattened over a card.

Surfaces
TokenLightDark
base
App canvas
stone/050
night/950
raised
Cards, panels, table body
white
night/925
overlay
Modals, popovers, dropdowns
white
night/900
sunken
Read-only fields, checkbox and radio box, neutral badge, avatar overflow
stone/100
night/950
scrim
Modal backdrop
alpha/mist-95
alpha/ink-95
inverse
Avatar fill
stone/950
stone/050
accent-subtle
Selected nav, highlighted row
twilight/050
twilight/900
success-subtle
Success badge
moss/050
moss/900
warning-subtle
Warning badge
amber/050
amber/900
danger-subtle
Error badge
ember/050
ember/900
info-subtle
Info badge
glacier/050
glacier/900

Text

Measured against surface/raised. Placeholder is the same stop as tertiary and clears AA; disabled and inert sit below it deliberately — both are exempt under WCAG 2.1, and both are documented at their recorded value so a future edit cannot quietly make them worse.

Text
TokenLightDark
primary
Headings and body
stone/900
16.28AAA
stone/050
16.83AAA
secondary
Labels, metadata
stone/800
12.28AAA
stone/300
10.51AAA
tertiary
Helper text, timestamps
stone/600
5.38AA
stone/400
7.55AAA
placeholder
Input placeholders
stone/600
5.38AA
stone/400
7.55AAA
disabled
Disabled text (WCAG-exempt)
stone/400
2.39Fail
stone/600
3.363:1
inert
Inert text beside interactive content, such as days outside the month (WCAG-exempt)
stone/300
1.72Fail
night/700
2.19Fail
inverse
Text on surface/inverse
white
1.00Fail
stone/950
1.08Fail
accent
Links
twilight/600
5.96AA
twilight/300
10.24AAA
success
Success messages
moss/700
7.73AAA
moss/300
10.92AAA
warning
Warning messages
amber/800
12.25AAA
amber/300
10.55AAA
danger
Validation errors
ember/700
8.86AAA
ember/300
10.16AAA
info
Info messages
glacier/700
7.85AAA
glacier/300
10.90AAA

Interactive

Fills and the labels that sit on them. Every on-* token is measured against all of its fill states, not just the resting one. The two wash tokens are a state layer rather than a fill — laid over a row, a menu item, a ghost button or the neutral button’s own fill — and are shown here over a card; their readings are on the Elevation page.

Interactive
TokenLightDark
accent
Primary button fill
twilight/600
5.96AA
twilight/400
7.18AAA
accent-hover
Primary hover
twilight/700
8.89AAA
twilight/300
10.24AAA
accent-pressed
Primary pressed
twilight/800
13.19AAA
twilight/200
12.99AAA
on-accent
Label on accent
white
1.00Fail
night/950
1.08Fail
neutral
Secondary button fill
stone/050
1.07Fail
stone/800
1.47Fail
on-neutral
Label on neutral
stone/900
16.28AAA
stone/050
16.83AAA
wash-hover
Hover wash over any surface or the neutral fill: rows, menu items, ghost, outline and neutral buttons, icon buttons
alpha/haze-08
alpha/haze-12
wash-pressed
Pressed wash, same consumers
alpha/haze-16
alpha/haze-20
tertiary
Highlight fill
flare/400
2.53Fail
flare/400
7.13AAA
tertiary-hover
Highlight hover
flare/300
1.76Fail
flare/300
10.24AAA
tertiary-pressed
Highlight pressed
flare/200
1.39Fail
flare/200
13.00AAA
on-tertiary
Label on highlight
stone/900
16.28AAA
night/950
1.08Fail
success
Confirming button fill
moss/600
5.09AA
moss/400
7.96AAA
success-hover
Confirming hover
moss/700
7.73AAA
moss/300
10.92AAA
success-pressed
Confirming pressed
moss/800
11.82AAA
moss/200
13.49AAA
on-success
Label on success
white
1.00Fail
night/950
1.08Fail
danger
Destructive button fill
ember/600
5.93AA
ember/400
7.04AAA
danger-hover
Destructive hover
ember/700
8.86AAA
ember/300
10.16AAA
danger-pressed
Destructive pressed
ember/800
12.97AAA
ember/200
12.96AAA
on-danger
Label on danger
white
1.00Fail
night/950
1.08Fail
selected
Selected row, tab, nav
twilight/050
1.08Fail
twilight/900
1.06Fail
disabled
Disabled fill — in Dark equals neutral; the label carries the state
stone/200
1.36Fail
stone/800
1.47Fail
on-disabled
Disabled label
stone/400
2.39Fail
stone/600
3.363:1

Border

Three tiers by function, not by weight. subtle divides, and is an alpha so it reads on every surface without picking a stop above any of them; default outlines containers; and strong is the only tier that clears WCAG 1.4.11 — which is why every form control uses it.

Border
TokenLightDark
subtle
Dividers, row separators
alpha/ink-08
1.18Fail
alpha/white-16
1.62Fail
default
Cards and containers — decorative
stone/300
1.72Fail
night/600
3.303:1
strong
All form control boundaries
stone/500
3.573:1
stone/500
5.06AA
accent
Active, selected
twilight/600
5.96AA
twilight/400
7.18AAA
focus
Focus ring — the only focus token
twilight/500
3.923:1
twilight/300
10.24AAA
danger
Error
ember/600
5.93AA
ember/400
7.04AAA
success
Validated
moss/600
5.09AA
moss/400
7.96AAA
inverse
On surface/inverse
stone/700
8.10AAA
stone/200
13.24AAA
info-subtle
Edge of surface/info-subtle
glacier/200
1.00Fail
glacier/700
1.00Fail
success-subtle
Edge of surface/success-subtle
moss/200
1.00Fail
moss/700
1.00Fail
warning-subtle
Edge of surface/warning-subtle
amber/300
1.00Fail
amber/700
1.00Fail
danger-subtle
Edge of surface/danger-subtle
ember/200
1.00Fail
ember/700
1.00Fail

Category

Colour by category rather than by meaning: a person’s events on the Scheduler, a tag by topic. Six hues, each the accent’s own four stops — 600 / 400 for the fill, white / night-950 for the label on it, 050 / 900 for the tint, 700 / 300 for the text on the tint or on a card — so whatever holds for the accent holds for each. The label is measured on its fill and the text on its tint, both to AA; the fill on a card to 3:1. The tint is a ground, and is not measured. Not a status: a warning is not “amber”.

Category
TokenLightDark
glacier
A category fill: a person's events, a topic
glacier/600
5.23AA
glacier/400
7.93AAA
on-glacier
Label on category/glacier
white
5.23AA
night/950
8.54AAA
glacier-subtle
The tint of category/glacier
glacier/050
glacier/900
glacier-text
Text and edge on the tint, or on a card
glacier/700
7.36AAA
glacier/300
9.70AAA
moss
A category fill: a person's events, a topic
moss/600
5.09AA
moss/400
7.96AAA
on-moss
Label on category/moss
white
5.09AA
night/950
8.57AAA
moss-subtle
The tint of category/moss
moss/050
moss/900
moss-text
Text and edge on the tint, or on a card
moss/700
7.25AAA
moss/300
9.57AAA
amber
A category fill: a person's events, a topic
amber/600
5.43AA
amber/400
7.53AAA
on-amber
Label on category/amber
white
5.43AA
night/950
8.11AAA
amber-subtle
The tint of category/amber
amber/050
amber/900
amber-text
Text and edge on the tint, or on a card
amber/700
7.62AAA
amber/300
9.49AAA
ember
A category fill: a person's events, a topic
ember/600
5.93AA
ember/400
7.04AAA
on-ember
Label on category/ember
white
5.93AA
night/950
7.58AAA
ember-subtle
The tint of category/ember
ember/050
ember/900
ember-text
Text and edge on the tint, or on a card
ember/700
8.22AAA
ember/300
9.45AAA
glow
A category fill: a person's events, a topic
glow/600
6.02AA
glow/400
6.97AA
on-glow
Label on category/glow
white
6.02AA
night/950
7.51AAA
glow-subtle
The tint of category/glow
glow/050
glow/900
glow-text
Text and edge on the tint, or on a card
glow/700
8.24AAA
glow/300
9.36AAA
flare
A category fill: a person's events, a topic
flare/600
5.74AA
flare/400
7.13AAA
on-flare
Label on category/flare
white
5.74AA
night/950
7.68AAA
flare-subtle
The tint of category/flare
flare/050
flare/900
flare-text
Text and edge on the tint, or on a card
flare/700
7.95AAA
flare/300
9.40AAA

Chart

The two ramps a chart needs beside the six categories: sequential for a magnitude, twilight in five steps, and diverging for a value on either side of a centre, twilight against flare on a stone centre. A step is numbered by its distance from the canvas, so sequential-1 and mid sit nearest it in both modes. The steps near the canvas are under 3:1 by construction — a heatmap’s low cell is told apart by the legend and its neighbours — and are recorded rather than graded; what the suite holds for them is the lightness between neighbours. The figures on both surfaces, the text a value inside a cell takes, and the rules are on Data visualisation.

Chart
TokenLightDark
sequential-1
Sequential ramp, the step nearest the canvas
twilight/200
1.39recorded
twilight/800
1.37recorded
sequential-2
Sequential ramp, step 2
twilight/400
2.52recorded
twilight/600
3.03recorded
sequential-3
Sequential ramp, step 3, the first to clear 3:1
twilight/500
3.923:1
twilight/500
4.61AA
sequential-4
Sequential ramp, step 4
twilight/600
5.96AA
twilight/400
7.18AAA
sequential-5
Sequential ramp, the step furthest from the canvas
twilight/800
13.19AAA
twilight/200
12.99AAA
low-3
Diverging ramp, furthest below the centre
twilight/800
13.19AAA
twilight/200
12.99AAA
low-2
Diverging ramp, two below the centre
twilight/600
5.96AA
twilight/400
7.18AAA
low-1
Diverging ramp, one below the centre
twilight/400
2.52recorded
twilight/600
3.033:1
mid
Diverging ramp, the centre
stone/200
1.36recorded
stone/800
1.47recorded
high-1
Diverging ramp, one above the centre
flare/400
2.53recorded
flare/600
3.153:1
high-2
Diverging ramp, two above the centre
flare/600
5.74AA
flare/400
7.13AAA
high-3
Diverging ramp, furthest above the centre
flare/800
12.72AAA
flare/200
13.00AAA

Accessibility

Colour is never the only channel. A state has an icon, a label or a shape beside its colour; focus adds a ring rather than recolouring a border; a selected row carries an attribute a screen reader can read. Text clears 4.5:1 and non-text, borders and icons and the parts of a control, 3:1, on the surface they render on, in both modes.

Two text tokens sit under the line on purpose. disabled and inert are exempt under WCAG 2.1 — one is an unavailable control, the other decoration — and each is documented at its recorded value above, so an edit that made either worse would show in the table before it shipped.