Brand colours are usually chosen on a swatch, in isolation. Then they are used for body text, buttons and links, on white and on black, where some of them turn out to be hard to read. It is far cheaper to check contrast while choosing the palette than to repair it after launch.
The rule
The Web Content Accessibility Guidelines (WCAG) measure legibility as a contrast ratio between two colours, from 1 to 1 for identical colours up to 21 to 1 for black on white.
| Content | Minimum ratio (level AA) |
|---|---|
| Normal text | 4.5 to 1 |
| Large text (about 24px, or 18.66px bold) | 3 to 1 |
| Interface parts such as input borders and icons | 3 to 1 |
Meeting these numbers helps everyone, not only people with low vision. It also keeps text readable in sunlight and on cheap screens.
The formula
The ratio compares the relative luminance of the lighter and darker colour, where luminance is a weighted sum of the red, green and blue channels after converting from sRGB.
function channel(value: number): number {
const c = value / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}
function luminance(hex: string): number {
const [r, g, b] = [1, 3, 5].map((i) => channel(parseInt(hex.slice(i, i + 2), 16)));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
export function contrast(a: string, b: string): number {
const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (light + 0.05) / (dark + 0.05);
}
contrast("#ffffff", "#000000"); // 21
contrast("#767676", "#ffffff"); // about 4.54, the lightest grey that passes on whiteWhere brand colours usually fail
- Bright accents as text. Yellow, orange and light green look lively but often fall below 3 to 1 on a white background. Use them as fills for buttons and shapes, and put dark text on top.
- The same colour in both themes. A mid blue that works on white may be too dark on a black background. Define a separate accent for the dark theme and check it on its own.
- Muted text. Secondary grey text is the most common failure. Check it against the page background, not against pure white.
- Text on images or gradients. The ratio changes across the image. Add a solid or semi-opaque layer behind the text.
A process that works
- Pick the neutrals first: a background, a surface, a text colour and a muted text colour, for each theme.
- Check every text colour against every background it will sit on.
- Choose the accent last, and test it two ways: as text on the background, and as a button fill with its label.
- Record the passing pairs in the design tokens, so nobody has to rediscover them.
Make it part of the build
Contrast problems return whenever someone edits a colour. A small test that runs the function above over your token file turns the guideline into a check that fails the build, the same way a type error would.
A palette that passes on day one and keeps passing is worth more than a clever colour story, and it costs only a few lines to protect.