Building an accessible color palette that passes WCAG contrast
A palette that looks beautiful in a mood board can still fail the moment you put text on it. Accessibility is not a filter you apply at the end; it is a constraint you design within from the first swatch. Here is a practical process for building a palette where every text and background pairing you ship meets WCAG contrast, without flattening the design into greys.
Start with hues, not shades
Pick your base hues first: usually one brand color, one or two accents, and a neutral. Work in HSL, because hue, saturation, and lightness map to decisions you can reason about. Hue sets the identity, and you will hold it nearly constant while you generate the rest of the scale.
Three or four hues is plenty. More hues means more pairings to test and more ways to drift off brand.
Generate tints and shades as a scale
For each hue, build a scale of steps, commonly nine or ten, from very light to very dark. The reliable way is to keep hue fixed and vary lightness, with a slight saturation adjustment at the extremes so the lightest steps do not look washed out and the darkest do not look muddy.
Label the steps by a number, not by name. A blue-100 to blue-900 scale travels well between tools and survives a rebrand, whereas sky and navy lock you into one interpretation.
The point of a scale is that contrast becomes predictable. Steps far apart on the scale have high contrast; adjacent steps do not. That lets you reason about pairings before measuring them.
Know the thresholds
WCAG defines contrast as a ratio between the relative luminance of two colors, from 1:1 (identical) to 21:1 (black on white).
| Content | AA minimum | AAA minimum |
|---|---|---|
| Normal text | 4.5:1 | 7:1 |
| Large text (18.66px bold or 24px) | 3:1 | 4.5:1 |
| UI components and graphics | 3:1 | not defined |
"Large" is the common surprise: it means 24px regular or about 18.66px bold, not merely a heading. Icons, form borders, and focus rings fall under the 3:1 component rule, which people often forget entirely.
Test the pairings you will actually ship
A palette is not accessible or inaccessible on its own. Only a specific foreground over a specific background has a ratio. So enumerate the pairings you actually use:
- Body text on the page background.
- Body text on each card or surface color.
- Link and button text on their fills.
- Placeholder and disabled text, which teams routinely let fail.
- White and dark text over each brand color, to decide which to use on buttons.
For each pair, check the ratio against the table above. From a numbered scale, a useful rule of thumb is that text needs roughly a five-step gap for normal body copy, but verify rather than trust the gap, because perceived lightness is not linear.
Fix failures by moving lightness, not hue
When a pairing fails, adjust the lightness of one color and leave hue and saturation where they are. Darkening a blue to pass contrast keeps it recognizably your blue; shifting its hue to cyan does not. This is the whole reason to work in HSL rather than nudging raw RGB channels.
Sometimes the honest fix is to not put text on that color at all. A vivid accent can be reserved for borders, fills, and large graphics, where the 3:1 rule applies, rather than forced to carry body text.
A workable process
- Choose three or four hues in HSL.
- Build a numbered lightness scale for each.
- Decide white or dark text for every colored surface.
- Check every real text pairing against AA, and AAA where you can reach it.
- Darken or lighten the failing color, holding hue, until it passes.
Do this once and record the passing pairs as tokens; then the whole team inherits an accessible palette instead of rediscovering the failures per screen.
To build and verify the scale, paste each candidate into the Color Converter, read its HSL, step the lightness, and run the built-in AA and AAA contrast check on each pairing before you commit it.