WebAIM scans a million home pages every year, and every year low-contrast text is the most common failure it finds, on about four pages in five. It is also the easiest failure to fix, because the check takes five seconds and the fix is usually one hex value.
This post covers what the contrast thresholds in WCAG mean, why your own eyes are the wrong instrument for judging them, the five places designs fail most, and how to set up a palette so that nobody has to check anything at the end.
Contrast ratio is a number from 1 to 21 that compares the brightness of two colours. Identical colours score 1:1 and the text vanishes. Black on white scores 21:1. Everything you ship sits in between, and the question is only where the line is.
The line is 4.5:1 for text and 3:1 for everything else
WCAG has three levels, A, AA and AAA, and AA is the one the laws reference, so AA is the target.
At AA:
- Normal text needs 4.5:1.
- Large text needs 3:1. Large means at least 24 pixels, or 18.66 pixels if bold.
- Non-text elements that carry meaning need 3:1: icons, the border of an input field, the focus ring, the bars in a chart. This rule arrived with WCAG 2.1 and is the one most designs still miss.
At AAA the text thresholds rise to 7:1 and 4.5:1. Aim for it on body text if you can; do not promise it across a whole site, because it rules out most colour on colour.
The numbers only become real when you test your own work. The mid-grey body text that looked refined in the mockup is 3:1. The muted blue links are 3.5:1. Both fail, and both are on most sites I test. Put the two hex values into the Contrast Checker and it gives you the ratio, a pass or fail per level and size, and the nearest passing shade of either colour so you do not have to hunt for it.
Meeting the threshold does not mean black on white. There are thousands of pairings that pass and look good. The constraint is real and it is narrower than designers like, but it is far from the choice between compliant and attractive that it is often presented as.

Why your eyes are the wrong instrument
The ratio is computed from relative luminance, which is how bright a colour is to a human eye that is far more sensitive to green than to red and to red than to blue. Each channel is converted from its screen value to linear light, then weighted:
`
L = 0.2126 R + 0.7152 G + 0.0722 * B
`
and the ratio between two colours is
`
(L1 + 0.05) / (L2 + 0.05)
`
with L1 the lighter of the two. The 0.05 stands for the glow a black screen still gives off in a lit room.
Nobody computes this by hand. The reason to know it is that it explains why you cannot judge contrast by looking. Your eyes adapt to whatever is on screen within seconds, so a pairing you have been staring at for an hour looks fine to you and to nobody arriving fresh. A designer on a bright calibrated monitor sees more separation than a user on a phone in the sun. And the green weighting means two colours that feel equally dark to you can be a full ratio point apart. The formula measures the screen; your eyes measure your eyes.
One honest caveat. The formula has known flaws: it overrates some dark-on-dark pairings and underrates some light text on mid-tones. The draft of WCAG 3 replaces it with a perceptual measure called APCA. Until that ships and the laws reference it, 2.x and the formula above are what an audit uses, so build to them.
For finding candidate colours, the Color Picker gives you hex, RGB and HSL for anything on screen, and HSL is the form you want, because adjusting lightness is how you move a colour across the threshold without changing its character.
The ratio is computed from relative luminance, which is how bright a colour is to a human eye that is far more sensitive to green than to red and to red than to blue.
The five places designs fail, in the order I find them
Placeholder text. The browser default is a light grey around #999, which on white is about 2.8:1. It is the first finding on almost every form I test. #767676 is the lightest grey that passes on white, at 4.54:1. Better still, stop using the placeholder as the label; a label that stays visible above the field passes contrast and survives the user starting to type.
Text on photographs. White text over a picture passes where the picture is dark and fails where a cloud is. The ratio is different at every pixel, so the only fix is to stop depending on the picture: a dark overlay behind the text, a solid panel, or the text moved off the image.
Mid-tone brand colours used as text. A cheerful orange, a standard material green, a light blue: all of them fail on white and fail on black. Keep the brand colour for large shapes and buttons where you control the background, and derive a darker shade of it for links and text. Ten to fifteen points lower in HSL lightness usually does it and nobody notices the difference.
Non-text elements nobody checked. The one-pixel grey border on an input, the focus ring, the icon that is the only way to open a menu. All need 3:1 against what surrounds them. Designers check text and forget these, and they fail the 2.1 rule that audits now test.
Dark mode by inversion. Pure white on pure black is 21:1 and it hurts. The text seems to glow and smear for people with astigmatism, which is a lot of people. Off-white on dark grey, say #E0E0E0 on #121212, is still comfortably above AAA and far easier to read for an hour.
Disabled controls are exempt from the rule, and it is still worth pairing the faded look with something else, a lock icon or a note, so that the user knows the control exists and why it is off.

Build the palette so the check is already done
Checking every pairing on every page is how contrast findings keep coming back. Check the palette once instead, and let the pages inherit the result.
Start from the backgrounds. Most sites have three or four: the page, a tinted section, a card surface, and a dark variant. For each background define the text colours that are allowed on it: one for headings and body, one for secondary text such as captions and dates, one for links, and one accent for icons and borders. Body and secondary text need 4.5:1 on that background, links need 4.5:1 and something other than colour to mark them, the accent needs 3:1.
Then make the matrix: every text colour against every background, with the ratio in the cell and a pass or fail for AA. It is a table with perhaps sixteen cells. Anything that fails is either fixed by moving its lightness or deleted from the system. What remains is the set of pairings anyone may use without checking, and the tokens in the CSS get those names, not the hex values.
A colour that is not in the matrix is not in the design system, and a pull request that introduces one gets sent back. That single rule, held to, is the difference between a site that passes an audit and a site that passes it once.
Most brand colours need one derived shade to get into the matrix. Take the brand hex into the Color Picker, drop the HSL lightness ten to fifteen points, and run the result through the Contrast Checker. The brand stays recognisable and the text passes.
Checking every pairing on every page is how contrast findings keep coming back.
What the scanners catch and what only a person catches
Run axe, WAVE or Lighthouse on a page and they find text on solid backgrounds, button labels, form labels and any icon whose colours are in the CSS. That covers most findings, which is why running one of them on every build is worth the minute it takes.
What the scanners cannot see:
- Text over a gradient or a photo. The ratio changes across the element and the scanner has no single pair of colours to compare.
- Text with transparency. The rendered colour is a blend the scanner does not compute.
- The focus ring. Scanners test the resting state; the focus state only exists when someone tabs to the control.
- Hover and active states, for the same reason.
- A component that is rendered later by script, after the scanner has finished.
For those, the test is manual and short: take a screenshot of the element in its real context, sample the two colours with the picker, put them in the checker. Tab through the page once with the keyboard and look at every focus ring against every background it lands on. This is a ten-minute pass I do on every acceptance round, and it finds something every time.
The one thing to change in the process is when it happens. A contrast finding raised in acceptance testing costs a design change, a code change and a retest. The same finding raised when the palette was built costs one hex value.
Contrast is about whether the text can be seen. Whether it can be understood is a separate question, and the Readability Checker answers that one with a sentence-length and vocabulary score for the same page.
FAQ
Does dark mode have its own contrast rules?
No, the thresholds are the same. What changes is comfort: white on black passes with room to spare and makes the text seem to glow for people with astigmatism. Off-white on dark grey, #E0E0E0 on #121212 or thereabouts, still clears AAA and is easier to read for a long stretch.
Can failing contrast get a company sued?
Yes. In the United States, web accessibility suits under the Americans with Disabilities Act run to thousands a year, and low contrast is on the list in most of them. In the EU the European Accessibility Act has applied since 28 June 2025 and points at WCAG 2.1 AA, with fines set by each member state. Contrast is the easiest item on any of those lists to fix, which is exactly why it looks bad when it is not.
How do I pass without changing the brand colour?
Keep the colour and change where it is used. A brand blue that fails for body text is fine for a large heading, for a button where you also choose the text colour, and for anything decorative. For body text and links derive a darker shade of the same hue. The brand guidelines will survive it; most already have a darker variant somewhere in the deck.
What about text that scrolls over a changing background?
The threshold has to hold at every point, so either give the text its own solid or translucent panel, or keep it in the parts of the layout where the background is reliably dark or reliably light. There is no trick that makes white text pass over a cloud.
### Does dark mode have its own contrast rules.
Type Scales: Pick One Ratio, Put It in CSS Variables, Stop Guessing Font Sizes
How a typographic scale works, which ratio suits which kind of site, the CSS custom properties that make it stick, and the line-height and letter-spacing rules that go with it. Written by someone who ships Tailwind defaults and is fine with that.
Aspect Ratio Calculator: Get the Other Dimension Right for Video, Image and Print
What 16:9, 9:16, 4:5 and 2:3 mean, the pixel sizes each platform wants in 2026, the difference between resizing and cropping, and the CSS that keeps a grid of images the same shape.
A Brand Color Palette That Survives Print, Email and Dark Mode
How many colors a brand needs, what job each one has, the contrast check that decides which shades may carry text, and how to write it all down so three developers do not ship three different blues.
Color Blind Simulator: Why the Red and Green in Your Status Column Read the Same to One Man in Twelve
One in twelve men cannot tell your red from your green. What the four kinds of colour vision deficiency do to a screen, the patterns that fail, the palette that does not, and a five-step test that fits in a design review.
