Skip to content

In progress · v0.1

Color system

Color is built on OKLCH: a perceptual color space where the same lightness value reads as the same brightness across every hue. That gives us predictable contrast, ramps that step evenly, and dark-mode swaps that just work.

Uniform lightness Every ramp shares L values across hues: a/500 green and a /500 blue sit at the same brightness.
Predictable contrast AA targets are reachable by reading L, not by trial and error.
Seamless modes A semantic token keeps its meaning and swaps to its homolog per mode, no re-picking.

The raw material. Eight OKLCH palettes, each a perceptually even ramp (same lightness reads the same across hues). Primitives are values, not roles: they never go into a design directly. A semantic token points at one, and the mode decides which.

GreenBrand & confirmation
50
#d4ffe0L 96
100
#baeec8L 90
200
#9bdbacL 84
300
#7cc892L 77
400
#5bb578L 70
500
#29a35aL 63
600
#008434L 53
700
#006312L 43
800
#004300L 33
900
#002500L 23
950
#000900L 12
TealAtmosphere · Navy surfaces
50
#edf7faL 97
100
#d3dfe1L 89
200
#b8c7c9L 82
300
#9fafb2L 74
400
#85989bL 67
500
#6c8285L 59
600
#4e6669L 49
700
#334a4eL 39
800
#1b3033L 29
900
#06181aL 19
950
#000405L 10
CreamThe warm ground
50
#f7f5f0L 97
100
#eae8e3L 93
200
#dedcd4L 89
300
#bebcb3L 79
400
#9f9d93L 69
500
#827f73L 59
600
#656256L 49
700
#49463bL 39
800
#2f2d22L 29
900
#17150cL 19
950
#040300L 10
NeutralStructure & text
0
#ffffffL 100
100
#dededeL 90
200
#bebebeL 80
300
#9e9e9eL 70
400
#808080L 60
500
#636363L 50
600
#484848L 40
700
#2e2e2eL 30
800
#161616L 20
900
#030303L 10
1000
#000000L 0
RedError only
50
#ffe6d9L 94
100
#ffcebfL 89
200
#fdb3a2L 83
300
#ed9985L 76
400
#de7f69L 69
500
#d16147L 62
600
#b3381eL 52
700
#8f0e00L 41
800
#690000L 33
900
#420000L 24
950
#1e0000L 15
YellowAction & warning
50
#fff09aL 95
100
#ffdd7aL 91
200
#ffc93dL 86
300
#e7a700L 77
400
#ce8500L 68
500
#b96000L 58
600
#9e4200L 49
700
#7a2f00L 41
800
#591a00L 32
900
#370800L 22
950
#200200L 16
LimeAccent, action in dark
50
#e2ff9dL 96
100
#d4fb84L 94
200
#c4f254L 90
300
#a0d300L 80
400
#7eb400L 71
500
#5b9500L 61
BlueInformation
50
#d9f8ffL 96
100
#bbddffL 88
200
#9ac3fcL 81
300
#79a9ebL 73
400
#598fd9L 65
500
#3375ccL 56
600
#0057b6L 47
700
#003a97L 38
800
#001d75L 29
900
#000050L 20
950
#030031L 15

Each token holds a role; the mode decides which primitive (its homolog) fills it. Switch modes below. The color and the ground change with it.

scheme
scheme/background
scheme/onBackground
scheme/surface
scheme/onSurface
scheme/onSurfaceVariant
scheme/surfaceContainerLow
scheme/surfaceContainer
scheme/surfaceContainerHigh
scheme/surfaceContainerHighest
scheme/primary
scheme/onPrimary
scheme/primaryContainer
scheme/onPrimaryContainer
scheme/secondary
scheme/onSecondary
scheme/secondaryContainer
scheme/onSecondaryContainer
scheme/outline
scheme/outlineVariant
scheme/inverseSurface
scheme/onInverseSurface
scheme/inversePrimary
brand
brand/action
brand/emphasis
feedback
feedback/error
feedback/onError
feedback/errorContainer
feedback/onErrorContainer
feedback/warning
feedback/onWarning
feedback/warningContainer
feedback/onWarningContainer
feedback/success
feedback/onSuccess
feedback/successContainer
feedback/onSuccessContainer
feedback/info
feedback/onInfo
feedback/infoContainer
feedback/onInfoContainer
text
text/primary
text/secondary
text/tertiary
text/disabled
text/accent
text/accentSubtle
text/link
button
button/primary
button/onPrimary
button/secondary
button/onSecondary
button/tertiary
button/onTertiary
button/disabled
button/onDisabled
stroke
stroke/default
stroke/strong
stroke/subtle
stroke/brand
stroke/inverted
avatar
avatar/identity/1
avatar/identity/2
avatar/identity/3
avatar/identity/4
avatar/identity/5
avatar/identity/6
avatar/onIdentity
  • Yellow is what you do next, green is what is already true. They never compete on the same screen. In dark modes, brand/action resolves to lime automatically.
  • brand/ has exactly two tokens: action (CTA) and emphasis (brand green).
  • on* lives in button/, not in brand/.

Our green has to be two things at once, and they pull in opposite directions. As a fill it carries text on top of it. As text it has to be read against a pale ground. A single stop cannot win both: to be legible as type it needs to be dark, and to hold light type it also needs to be dark. Green/500 sat in the middle and fell short of both.

So we split the green by role. The brand stop never moved.

roletokenLightwhy
brandbrand/emphasisGreen/500 #29a35auntouched, this is the brand
texttext/accentGreen/600 #008434the most chromatic stop in the whole ramp
linktext/linkGreen/700 #006312links are body text, so they need the safe stop
linestroke/brandGreen/600 #008434lines answer to 3:1, not 4.5:1

There is no button row. Green had one for a while, and then it lost it: see Green is not any button’s fill below.

text/accent is Green/600, which is not just darker than the brand stop but more saturated than it (chroma 0.154 against 0.151). It reads greener, not duller. The trade is that it clears AA in two places only:

  • Headings of 24px and up, where the bar drops to 3:1.
  • Body text on scheme/surface (white cards), at 4.83:1.

On the cream scheme/background at body size it lands on 4.43:1 and misses by 0.07. That is why links use Green/700 instead: a link lives inside a paragraph, on the page ground, every time.

Do not reach for text/accent in running text over the cream background. If the green has to survive at body size anywhere, it is text/link.

The 3:1 bar only opens up for 24px and above at normal weight, or 18.66px and above in bold. Nothing below that qualifies, bold or not. In our scale the line falls between headlineSmall (24px, large) and titleLarge (22px, normal).

The rule for a button is its label. 4.5:1 against its own fill, always. That is the figure that decides whether someone can read and press the thing, and it is the only contrast figure a labelled button has to answer for.

The fill against the page behind it does not have to reach 3:1. This surprises people, so it is worth quoting: where a control has visible content that identifies it, W3C’s Understanding of SC 1.4.11 says an “indication of the overall boundary of the hit area is not required”. A pill carrying a readable label is identified by that label. The boundary is ours to design.

That is why our yellow call-to-action is fine on cream even though the formula computes 1.41 against it. WCAG 2 measures lightness only, and yellow and cream sit close in lightness while being worlds apart in saturation. The button is the most visible thing on the page. The formula cannot see that.

The exception is a button with no visible label. An icon-only button has to be identified by its icon, so that icon needs 3:1 against the fill. And an empty input field has no visible content at all, which is why its border is covered by the 3:1 rule.

typefilllabelratio
primarybrand/action #ffc93dbutton/onPrimary, black13.67 ✅
secondarybutton/secondary #dedcd4button/onSecondary, black15.29 ✅
tertiarybutton/tertiary, which resolves to surface #fffffftext/primary, black21.00 ✅

Tertiary is the one type whose fill matches the ground under it, so its border is what draws the button at all. That border is stroke/strong, and because it is carrying the shape it does have to clear 3:1. It reads 3.95 in Light, 4.59 in Carbon and Navy.

Green used to be the secondary fill. It is not any more, and that is the largest change of August 2026. The short version: yellow is what you do next, green is what is already true. A button is something about to become true, which is the opposite tense.

Green still carries plenty on screen: selection, checkmarks, active nav, accented words, amounts, chevrons, spinners, progress, links. Nine jobs, and every one of them states a fact. Giving it a tenth job where green asks to be tapped is exactly how the colour emptied out in the old app.

A contrast argument pointed the same way. White on Green/500 never got past 3.24:1, so the label had to be black regardless. brand/emphasis stays Green/500: the brand green did not move, the button simply stopped being it. The full reasoning is in ADR-013.

We still put buttons on scheme/surface rather than straight on scheme/background. That one is a design call, not a conformance one: a filled button simply reads better with a card under it.

The full per-mode token tables are generated from tokens/Color _ Semantic.json. See Tokens → Overview.