The palette
This page is about the layer underneath the tokens. The scale is not exposed, so it is hidden from the Figma picker and is not part of the public token set. Read it when you want to know why a token looks the way it does.
How the scale is built
No individual colour in the scale is hand-picked. Each one is three OKLCH numbers, assembled from three decisions taken once: a hue angle per anchor, a chroma curve shared by every hue, and one lightness value per step, shared by every hue and every tone.
There are seven hue anchors, each one a single hue angle that does not change across the 15 steps or
between schemes: blue, gray, green, moss green, north sea, orange and red. Those are
the names in the token source, which is why gray is spelled the American way there and "grey"
everywhere else on this page.
OKLCH is a way of writing a colour as three numbers: L for how light it is, C for how saturated, and H for which hue. It replaces hex and RGB, where the same three numbers tell you nothing about how a colour will look. The property we rely on is that L means the same thing across every hue, so changing H does not change how light the colour appears. That is what lets every tone's step 9 read as equally solid whatever colour it is.
red is the most saturated at every step, north sea the least, and gray none at all. Steps 14 and 15 break the run because they are foregrounds meant to sit on a fill rather than positions along it. blue and north sea sit only 2.3° apart, which is why north sea can carry neutral on a dark page without the interface changing character.Chroma is one curve per scheme, scaled by a single constant per hue: saturated through the middle
of the scale and muted at both ends. Because the shape is shared, saturation is never set per step or
per tone. red carries more chroma than north sea at every step, and gray carries none.
Lightness is the part that is chosen rather than derived. Every hue takes the same L at the same step, so it is set once for the whole scale, but the steps are deliberately uneven, and in dark mode they do not even rise in order. The steps jump on purpose explains why.
Every tone is built the same way. A step number means the same thing in each: the same lightness, and the same position on the chroma curve. So swapping a tone changes the colour and nothing else. What varies is the hue and how much chroma it carries, both set once per hue anchor.
What the step numbers mean
Each tone has 15 steps. The obvious reading, "1 is lightest and 15 is darkest", is not how they work.
A step number is a role position, not a lightness. Step 9 is "the solid fill". Step 15 is "the foreground that sits on a solid fill". That is true in both schemes, and it is what lets one set of token names work in light and dark with no overrides.
Steps 1 → 13 broadly travel from closest to the background toward furthest from it, which means descending lightness in light mode and ascending in dark.
"Broadly" is carrying weight in that sentence. In dark mode the lightness values dip at steps 4 and 5 on purpose, because a strictly increasing ramp cannot hold the contrast that every role at those steps needs.
So global ordering is not a rule and the dip is not a bug. The two rules that do hold are narrower:
- No two steps within one scale resolve to the same value.
- Every sequence of states progresses visibly in one direction.
Steps 14 and 15 sit outside the second rule, as described below. The first holds for them too.
Which step does what
Every tone, every step. Reading down a column shows the jump where strokes stop and fills begin; reading across a row shows that a step is the same role in every tone.
Steps 14 and 15 are inverse-polarity
They sit outside the ordering by design: their lightness runs against the direction of steps 1 to
13. That is the criterion, not what a step is used for, which is why step 1 can hold text.inverted
and icon.inverted and still be in order. Both must be excluded from any check on state order.
Step 15 serves two roles at once: the on-emphasis foregrounds that sit on a solid fill, and the
panel surface, which is why background.surface, background.dialog and background.floating all
resolve to it. That is the point of the reversal rather than a conflict, because it makes the panel
lighter than the canvas in light and darker in dark.
The steps jump on purpose
The scale is not a smooth gradient. Lightness values are chosen per step for intended pairings, such as text against surface and fill against canvas. Check contrast against the actual background for every state rather than assuming that all combinations meet the same target.
Notice that the middle of the scale is evenly spaced while the run from borders into fills jumps roughly twice as far. That gap is where strokes stop and fills begin, and it needs to be visible.
Tone to hue
The mapping from a tone to a hue happens per scheme, which is what allows neutral to be a different
hue in each:
| Tone | Light | Dark |
|---|---|---|
accent | moss green | moss green |
neutral | gray | north sea |
info | blue | blue |
success | green | green |
warning | orange | orange |
danger | red | red |
Dark-mode neutral is north sea rather than grey on purpose: a pure-grey dark interface reads as dead, and the slight blue keeps large dark surfaces feeling like a material.
Data visualisation
data-visualization.* is a separate group with its own rules, because chart colour is about telling
series apart rather than about contrast against a background.
Ten hues, five steps each. Use the hues to separate series; use the steps within one hue when a single series needs shading.
For magnitude, where one end means more than the other.
For values either side of a midpoint, such as above and below a target.
These are the only values in the set that sit outside the OKLCH generator, and no EDS component binds them. They are for you to apply in charts.
Why you cannot extend the scale yourself
A step number is a position, not a name for a particular colour. Inserting one moves every position above it, and most semantic tokens are defined by step number, so each of those would resolve to a different colour while the name a consumer binds to stayed exactly the same.
If you need a value the scale does not have, that is a token request rather than something to generate locally.