Bhinish Dhiman
How to fix colour contrast without changing your brand colour
Move the colour along its own lightness axis instead of picking a new one. Contrast is driven almost entirely by lightness, and hue survives a lightness change — so a darker or lighter version of your brand colour will pass while still reading as your brand.
Every accessibility audit produces the same awkward moment. The brand blue fails on white, the checker says 3.02:1, and the advice on offer is “choose an accessible colour.” Nobody who has sat in that meeting has found this useful. The colour is in the logo. It is in the deck. It was signed off by someone who is not in the room.
The good news is that the advice is wrong, or at least badly incomplete. In most cases you can keep the colour.
Contrast barely cares about hue
A contrast ratio compares relative luminance — roughly, how much light the colour emits. Luminance is overwhelmingly a function of lightness. Hue contributes a little, because the eye is more sensitive to green than to blue, and saturation contributes almost nothing.
The practical consequence is worth stating plainly: you can change how light a colour is without changing what colour it is. A darker blue is still recognisably your blue. It carries the same brand association, sits in the same family, and pairs with the same secondary colours. What it does not do is fail your audit.
This is why “pick a different colour” is such poor advice. It treats the colour as a single indivisible thing, when in fact it has one property that matters for contrast and two that matter for identity.
Work in OKLCH, not hex
If you nudge a hex value around by hand, you will change hue and saturation without meaning to. Hex is a storage format; it is not a useful space for reasoning about colour.
OKLCH separates the three properties cleanly:
- L — lightness, 0 to 100%. This is the one that moves contrast.
- C — chroma, roughly saturation.
- H — hue, 0 to 360°. This is the one that carries your brand.
Hold H, hold C where you can, and move L. Because OKLCH lightness is perceptually uniform — unlike HSL, where 50% lightness means something different for yellow than for blue — equal steps in L look like equal steps to the eye. That makes the adjustment predictable.
CSS accepts OKLCH directly, so you can ship the result without converting back:
--brand: oklch(68% 0.14 240); /* fails on white */
--brand-text: oklch(52% 0.14 240); /* same hue, passes AA */
Two custom properties, one hue. The brand colour stays the brand colour and gains a variant that is safe for text.
How far you have to move
Less than people expect. Going from 3:1 to 4.5:1 against white is typically a lightness change somewhere around 8–15%, depending on the hue. That is a visible change if you put the two side by side, and essentially invisible if you do not.
There is a real limit, and it is worth knowing where it is. Highly saturated warm colours — yellow, orange, light green — have a low ceiling against white, because they are intrinsically light. A saturated yellow cannot reach 4.5:1 against white at any lightness while remaining saturated yellow. When you hit that wall, the honest options are to reduce chroma, or to change the background instead.
Changing the background is usually the smaller compromise
This one is under-used. When the brand colour is the text, the constraint is the brand colour. When it is the background, you have two things you can move, and the text is usually the less precious of them.
A button with a brand-coloured background and white text that fails is often fixed better by darkening the background slightly than by abandoning white text. Most tools never raise this, because they are built to evaluate a pair rather than to solve it.
Check the result at the size you are actually shipping
WCAG 2 compares two colours and ignores everything else. That means a 12px caption in Light and a 32px heading in Bold get the same score, which anyone who has shipped a technically-passing caption knows is not how reading works.
APCA, the candidate algorithm for WCAG 3, scores size and weight as well as colour. It produces some genuinely uncomfortable results. 14px at regular weight cannot pass at any contrast — not even pure black on pure white. It needs 16px, or it needs bold. WCAG 2 will wave that caption straight through.
Neither figure is sufficient alone. WCAG 2 is what conformance is measured against today. APCA is the better predictor of whether a person can read the thing. Where they disagree, ship to whichever is stricter.
Dark mode deserves its own pass
Do not assume a palette that passes on white will pass inverted. The WCAG 2 formula adds a fixed flare constant that dominates at the dark end, which means pairs near black can post a comfortably passing ratio and still read as dim and muddy. This is one of the specific failures APCA was designed to correct.
In practice, a brand colour usually needs two variants: one darkened for light backgrounds, one lightened for dark ones. Same hue, same chroma, two lightness values. That is a colour system rather than a colour, and it is what shipping in both themes actually requires.
The short version
- Keep the hue. Move the lightness.
- Work in OKLCH so you only change the thing you meant to change.
- If the colour is a background, consider moving it rather than the text on it.
- Check at the real size and weight, not just as two swatches.
- Generate a second variant for dark mode rather than assuming the first one inverts.
The contrast checker does all five. It draws the lightness axis of your own hue, shows which stretches of it pass, and gives you the nearest passing value ranked by how little the colour moves.