Colour contrast looks like the simplest accessibility rule there is. Two colours, one ratio, a number to beat. In practice it is the failure we have paid for four separate times on our own work, and every time the cause was the same: a colour was judged against the background someone declared, not the background it actually lands on.
This guide is the technical piece we know best, because we have been wrong about it in public and then measured our way out.
The numbers, first
The Web Content Accessibility Guidelines set the thresholds. At level AA, the level UK procurement and every serious buyer asks for:
- Normal text: at least 4.5∶1 against its background (success criterion 1.4.3).
- Large text: at least 3∶1 — where "large" means about 24 pixels, or about 19 pixels if it is bold.
- Interface parts and meaningful graphics: at least 3∶1 (success criterion 1.4.11). Input borders, icon buttons, focus rings, the slice of a chart that carries the meaning.
At level AAA, text rises to 7∶1, and large text to 4.5∶1 (success criterion 1.4.6).
Two things follow immediately. Muted grey secondary text — the thing every modern design uses to make a page feel calm — is where AA breaks, because "muted" means lowered contrast. And logos are exempt, which is exactly why so many sites end up putting the logo colour on the body copy and assuming the exemption travels with it. It does not.
Why a colour passes on one background and fails on another
A contrast ratio is a property of a pair. Change either colour and the number changes. That sounds obvious written down; it is not obvious at all in a design system, because a colour is usually defined once, as a token, and then used in fifty places on eleven different backgrounds.
Here is the concrete version, from our own demonstration sites. A brand accent was being used for section eyebrows — the small uppercase label above a heading. The palette said the page background was one colour. But those eyebrows did not sit on the page background: they sat on a tinted band, a slightly different shade used to separate one section from the next. Measured against the declared background, the accent was fine. Measured against the band it was actually painted on, it was 2.55∶1 — barely half of what small text owes.
We had made the same mistake one layer up, on this website, twice: chasing a gold across two backgrounds and clearing it on the flat canvas only, then finding it failed on the panels; and re-tuning a cyan against the page colour while its real grounds gave between 4.10∶1 and 4.36∶1.
⛔ The rule, stated so it survives: a colour is judged against the ground it actually lands on. Not the token it derives from, not the page background, not the value in the brand guidelines. Find the pixel behind the letter.
The trap that makes this worse: the ground can be made of the colour itself
On one of our sites, the tinted band the eyebrow sat on was itself derived from the accent colour. Darken the accent to fix the text and the band darkens with it, so the ratio barely improves. That case could not be solved by arithmetic at all — the value had to be chosen with headroom and then the whole audit re-run to see where it actually landed.
If your design system tints backgrounds from brand colours — and most do — you cannot calculate your way out of a contrast failure. You have to measure the result.
Light and dark need corrections in opposite directions
This is the part that catches experienced teams. When a colour fails as text:
- On a light background, the fix is to make the colour darker.
- On a dark background, the fix is to make the colour lighter.
Two themes, one token, two opposite corrections. Which means a single brand value cannot serve both, and any palette that tries will fail in one of them. When we fixed five demonstration sites in one pass, that was the shape of the whole job: light canvases got darker accents, dark canvases got lighter ones, and the bright fill version of each colour was left untouched.
Which leads to the discipline that prevents the whole class of failure:
Split every brand colour into a TYPE colour and a FILL colour, and never let them swap jobs.
The fill is the loud one: logos, keylines, big numerals, a solid button someone puts white text on. The type colour is a darkened or lightened sibling that clears 4.5∶1 on the grounds it will actually be used on. Two tokens, two jobs, and the design keeps its personality where personality is visible — in the shapes, not in the small print.
We learned this the expensive way: our own design tokens had a note saying, twice, that the bright variant was "a fill … never type". The contract existed as a comment, and the rendered page disagreed with it. A rule that is written down but not measured is not a rule.
Two silent ways a passing colour goes back to failing
Transparency. Writing a colour at 70% opacity does not make the text 30% lighter in a way anyone tracked — it blends the text into whatever is behind it and quietly undoes the darkening someone applied months earlier to clear AA. We had exactly this: a muted token, carefully corrected, then used with an alpha modifier that reversed the correction. Nothing in the code looks wrong.
Inheriting a palette meant for the other theme. Utility frameworks ship dark-mode palettes and light-mode palettes. Apply the dark one unconditionally and it will look immaculate in dark mode and render white on near-white in light mode. That single mistake produced 202 of the 302 light-theme failures we found on this site.
Why your testing tool says nothing about your hero
Automated engines are strict about not guessing. When a testing engine cannot resolve what is behind a piece of text — a gradient, a photograph, a semi-transparent panel, a video — its contrast rule returns the element as "needs review" rather than as a violation. On a run our own demo audit reports as having no detectable failures, it hands back 1,540 such elements.
So the pattern is: the text most likely to have a real contrast problem is exactly the text your scanner will not judge. Hero copy over an image. A price over frosted glass. A caption on a gradient.
The only way to close that is to measure the pixels: render the page, screenshot it, sample the colours actually painted behind each piece of text, and compute the ratio from those. That is what we built after the scanner told us it could not decide — and the first time we ran it across every template of this site in both themes, it returned 308 findings across 6,502 elements, of which 306 were real.
How to fix a palette properly, in order
- List the real grounds. For each colour used as text, write down every background it actually appears on — including tinted bands, cards, overlays and hover states. These are usually not in your brand document.
- Measure against the worst one. Any contrast checker will do the arithmetic; the skill is choosing the right pair.
- Split type from fill. Correct the type value until it clears 4.5∶1 on the worst ground, with headroom. Leave the fill alone.
- Correct each theme separately, in opposite directions. Darker on light, lighter on dark.
- Re-measure the rendered page, not the tokens. If your backgrounds are derived from your brand colours, the arithmetic will be wrong. Re-run the whole audit.
- Check the greyscale screenshot. It is the cheapest sanity test available and it approximates what a colour-blind visitor sees before you have asked anybody for a ratio.
Where Neubor fits
Both of the gates described here run on every deployment of this website and of all seven of our public demonstration sites — the automated audit at phone and desktop widths, and the pixel-level contrast probe in both light and dark themes. A red result stops the release. We publish what they measure, including the 306 failures they found in our own pages, because the alternative is asking you to take a supplier's word for it.
If your brand colour is fighting your text, get in touch — it is usually a two-token fix and a re-measurement, not a redesign. Related: the eight faults we find on almost every small-business website and what an accessibility audit actually tests.
Kieran Bourne is the founder of Neubor, a Stoke-on-Trent web development and AI studio. Every ratio quoted here was measured on Neubor's own work.
