Building a color ramp that survives contact with real UI
One brand color is not a palette. Here is how the 50–950 scale works, why the ends always look washed out, and which step you should actually be using for text.
↑ Click any of these to repaint the entire site with it.
Somebody hands you a brand color. Not a palette — one hex code, from a logo, probably chosen years ago by someone who has left. Your job is to turn it into a system that can render a whole product.
The standard answer is an eleven-step ramp: 50, 100, 200, ... 900, 950. Tailwind popularized the shape and it has become close to universal. It is a good shape. It is also routinely generated badly.
What the numbers mean#
The steps are lightness, not saturation. 50 is nearly white, 950 is nearly black, 500 is the middle and is usually closest to the color you were given. The spacing is deliberately uneven — the steps are closer together at the light end, because the eye discriminates more finely there.
The three mistakes#
Interpolating toward white and black in sRGB. Mix your brand color with white in sRGB and the light steps go chalky and slightly gray, because you are averaging gamma-encoded numbers. Do it in OKLab and they stay clean.
Holding saturation constant. If every step has the same chroma, the light end comes out as a neon wash and the dark end as a bruise. Chroma needs to follow a curve that peaks somewhere around 400–600 and eases off at both ends.
Ignoring the gamut. There is a hard ceiling on how much chroma sRGB can display at a given lightness and hue, and it is much lower at the extremes. If you ask for chroma the display cannot produce, the value gets clipped — often into a slightly different hue than you asked for. Clamp deliberately instead of letting the clip decide.
Get those three right and the ramp comes out looking like one material at eleven brightnesses, which is the entire goal.
Which step for which job#
This is the part that generators do not tell you, and it is the part you actually need.
| Step | Job |
|---|---|
| 50 | Page or section background, light theme |
| 100 | Hover state on a 50 surface; subtle fills |
| 200 | Borders on light surfaces; disabled fills |
| 300 | Stronger borders; placeholder text on dark |
| 400 | Accent text and links, dark theme |
| 500 | The nominal brand color; rarely correct for text |
| 600 | Primary button fill; link color, light theme |
| 700 | Button hover; text that must clear AAA |
| 800 | Heading text in a tinted section |
| 900 | Section background, dark theme |
| 950 | Page background, dark theme |
The row worth memorising: 500 is almost never a text color. A mid-lightness color on white typically lands between 3:1 and 4:1 — fine for a large heading, a failure for body copy. The first step that reliably clears 4.5:1 on white is usually 600 or 700.
Neutrals belong to the palette too#
A ramp of pure grays next to a chromatic ramp looks wrong. The grays read as dead.
Build them from the same hue with a trace of chroma — 0.005 to 0.015 in OKLCH. Nobody will consciously see the tint. Everybody will notice that the interface feels coherent.
--gray-100: oklch(96.7% 0.008 258);
--gray-500: oklch(55.6% 0.010 258);
--gray-900: oklch(20.5% 0.008 258);Two layers of naming#
The ramp is primitives. Do not let components reference it directly.
/* Layer one — literal, generated */
--blue-600: oklch(55% 0.19 258);
/* Layer two — semantic, hand-authored, what components use */
--color-action: var(--blue-600);
--color-action-hover: var(--blue-700);
--color-border: var(--gray-200);If components reference --blue-600 directly, a rebrand is a find-and-replace across your entire codebase and you will miss some. If they reference --color-action, the rebrand is one line. This is the whole reason the two-layer pattern exists, and it costs nothing to adopt on day one and a great deal to retrofit on day six hundred.
One honest caveat#
An eleven-step ramp is a lot of colors, and most products use six of them. Generating all eleven is still worth it — the ones you do not use cost nothing, and having them documented stops the next person inventing a twelfth by hand at two in the morning.