Building a nine-step scale from one brand colour

A usable palette is a single hue rendered at a range of lightnesses, with each step tested for contrast rather than assumed. Take a teal of #1F6E60 and generate nine steps around it: of those nine, exactly two reach the 4.5:1 contrast ratio that body text on a white background requires. Everything else is a background, a border or a large-text colour.

That ratio — two usable text colours out of nine — is typical, and it is the reason palettes generated by eye tend to fail an accessibility audit.

What does the scale look like?

Nine steps from the same hue, with their contrast against white and against black.

Step Hex On white On black
1 #ebf9f7 1.08 19.43
2 #c3eee7 1.26 16.70
3 #9ce3d6 1.46 14.37
4 #74d8c6 1.70 12.38
5 #4ccdb6 1.96 10.72
6 #32b39c 2.60 8.06
7 #278b7a 4.15 5.06
8 #1c6357 7.07 2.97
9 #113c34 12.21 1.72

Steps 8 and 9 pass 4.5:1 on white. Step 7, at 4.15, fails for body text and passes the 3:1 threshold for large text — which is the kind of near-miss that gets waved through and then flagged in an audit.

Read the last column for the dark-mode equivalent. On a black ground, steps 1 to 6 pass comfortably and steps 8 and 9 fail badly, so a dark mode built by inverting a light palette inverts which steps are legal.

Why generate the scale in OKLCH?

Because OKLCH is perceptually uniform: equal steps in its lightness channel look equally different to the eye, which sRGB does not manage. A scale built by mixing toward white in hex produces steps that bunch up at one end and jump at the other.

The base colour here is oklch(48.7% 0.078 178.5). Holding the hue at roughly 178 and varying the lightness gives a scale that reads as one family, and the small chroma variation across the steps is deliberate — a colour at 97 per cent lightness cannot carry the same chroma as one at 32 per cent and stay inside the sRGB gamut.

Which contrast threshold applies where?

Three numbers cover it.

  • 4.5:1 for normal body text. This is the one that matters most and passes least often.
  • 3:1 for large text — roughly 18.5px bold or 24px regular — and for user interface components and graphical objects.
  • 7:1 for the stricter AAA level, which most brand colours cannot reach on white without becoming very dark.

A caveat worth carrying: the WCAG 2.2 formula is a poor model of how contrast actually looks. It is generous to mid-greys and harsh on saturated blues, so a marginal pass is a reason to look at the thing rather than a verdict on it.

What about the dark mode version?

It is a second scale, not a flipped one. Reading the table again shows why: the steps that pass on white are exactly the ones that fail on black, so a dark theme needs its accent taken from the pale end of the ramp rather than the dark end.

That is a design decision as much as an arithmetic one. A pale teal on a near-black ground reads as a different colour from a deep teal on white even though both come from the same hue, and treating dark mode as an inversion is what produces themes that feel washed out or acidic.

How many steps do you actually need?

Nine is a common choice because it maps cleanly onto the jobs a colour has to do: two or three pale steps for backgrounds and hover states, two or three mid steps for borders, fills and charts, and two or three dark steps for text and icons.

Fewer than seven and the scale runs out before covering all three jobs. More than eleven and adjacent steps become indistinguishable, which means the palette has more entries than decisions and people start picking arbitrarily.

The grey scale is the one to build first and the one most often left to chance. Most of any interface is grey, and a grey ramp that shares a hint of the brand hue holds a design together in a way a neutral ramp does not.

Questions people ask

Can I paste rgb() or hsl() in? Yes. They all describe the same colour and convert between each other exactly, so the format is a matter of what your codebase uses.

Does the eyedropper work everywhere? It depends on browser support for the native colour-picking API, which is not universal. Where it is missing, the hex field is the fallback.

Why does the scale keep the hue but not the exact colour? Because it is a scale through the hue rather than a set of tints of one specific colour. Your original sits within it as one of the steps, not necessarily at the centre.

What about colour blindness? Contrast is a separate question from hue discrimination. A palette that passes contrast can still be unreadable if it relies on distinguishing red from green, which is why colour should never be the only carrier of meaning.

Generate the scale, then test each step rather than trusting the family. The colour picker shows contrast and every format for any colour you enter, the image resizer is where those colours end up in exported assets, and the QR code generator is a reminder that some things need contrast far above 4.5:1.