Ttooleras
Color Tools

What the WCAG contrast ratio doesn't tell you

The WCAG 2.1 contrast formula comes from 1970s CRT broadcast engineering. Here's why #777 gray can fail on white, when the ratio lies, what APCA fixes for WCAG 3, dark-mode-specific failure modes, and a real debugging workflow for six common contrast problems.

Tooleras22 min read4,743 words
Advertisement

A designer handed me a mock and said the grays looked fine. The audit tool said four of them failed WCAG AA. The designer's instinct wasn't wrong. The tool wasn't wrong either. They were measuring different things, and the difference is the whole reason this post exists.

The WCAG 2.1 contrast ratio is a mathematical formula from 1970s television engineering that's been applied to text on phone screens in 2026. It's the closest thing we have to a universal accessibility yardstick, and it's saved countless users from unreadable interfaces. It's also a tool that sometimes lies — says things pass when they shouldn't and fails things that look fine. If you've ever argued with a contrast checker and felt you were right, you probably were about half the time.

This post is for the engineer whose audit just came back with contrast failures, the designer who doesn't want to hand over a grayscale palette just to pass a robot, and the accessibility lead trying to do this honestly. We'll walk the math (so you understand why the tool says what it says), cover the six failure cases you'll hit in real products, and finish with where the field is heading — which is APCA, a new algorithm that's a candidate for WCAG 3 and disagrees with the current rules in ways that usually make your users happier.

Along the way, I'll point to tools that do this math for you, the most obvious being our Color Contrast Checker — it runs in your browser, no signup, and gives you the ratio and pass/fail instantly. But knowing which answers to trust is more useful than having another tool, so let's start with the formula.

The formula, in plain English

WCAG's contrast ratio is two numbers divided by each other. The numerator is the "relative luminance" of whichever of your two colors is lighter, plus 0.05. The denominator is the relative luminance of the darker color, plus 0.05. The output is always a number between 1 (same color) and 21 (pure white on pure black).

Relative luminance, though, is not what you might think. It isn't "how bright does this look." It's a specific calculation:

function luminance(r, g, b) {
  const [R, G, B] = [r, g, b].map((c) => {
    c /= 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}

function contrast(rgb1, rgb2) {
  const L1 = luminance(...rgb1);
  const L2 = luminance(...rgb2);
  const [lighter, darker] = L1 > L2 ? [L1, L2] : [L2, L1];
  return (lighter + 0.05) / (darker + 0.05);
}

That weird piecewise bit in the middle — the c / 12.92 vs the Math.pow(...) — is a gamma correction. It's undoing the fact that sRGB, the color space your screen uses, is non-linear. The coefficients 0.2126, 0.7152, 0.0722 weight red, green, and blue by how much each contributes to perceived brightness: green matters most, red second, blue barely. This is why pure yellow on white has abysmal contrast even though yellow "looks bright" — yellow is red plus green, and both are already bright.

Here's the part nobody mentions: those luminance coefficients are from ITU-R BT.709, a broadcasting standard from 1970. They were chosen to match the phosphor primaries of consumer CRT televisions of that era. The formula has been tweaked once — the + 0.05 was added to the numerator and denominator to account for screen glare (your phone screen reflects some ambient light even at pure black, and 0.05 is a rough average). But it's fundamentally a formula designed for TV broadcasting in the Ford administration, applied to OLED phones under fluorescent office lighting.

This isn't a conspiracy — the WCAG working group knows this, which is part of why APCA exists. But if you've ever wondered why the math feels weirdly rigid, that's why. It's surprisingly old.

The four thresholds, and the exceptions nobody mentions

Here are the numbers you need. This is the whole spec, minus the footnotes:

TextAA (WCAG 2.1 minimum)AAA (WCAG 2.1 enhanced)
Normal text4.5:17:1
Large text (≥ 18pt OR ≥ 14pt bold)3:14.5:1
UI components, icons, graphical objects3:1 (WCAG 2.1 SC 1.4.11)

"Large" means 18 point or larger, or 14 point or larger if bold. Most people hear "18 pixels" and panic — it's 18 points, which is 24 pixels at the default 96 DPI. The distinction matters because it means body text in almost every real product has to hit 4.5:1, not 3:1.

The exceptions that matter in practice:

  • Logos and brand names are exempt. You can put your logo in whatever color you like. Text that's part of the logo also doesn't have to meet contrast — but text placed near the logo does.
  • Disabled UI is exempt. Disabled buttons, inactive form fields, and placeholder-state elements don't have to pass contrast. This is often abused — see the disabled button case below — but the spec is clear.
  • Decorative text is exempt. If a word is arranged to be visual texture rather than conveyed information, it doesn't need to pass. Good luck defending that in an accessibility lawsuit, though.
  • Incidental text is exempt. Text that happens to be captured in a photograph (a street sign in a travel photo, a book title in a product shot) doesn't have to hit 4.5:1.

The most common mistake I see: people read "large text = 3:1" and apply it to their whole page body. H1s and H2s can usually use 3:1. Your body paragraph cannot.

The edge that isn't really an edge

Here's a thing to try. Open our contrast checker in another tab and punch in #767676 on #ffffff. The ratio is 4.54:1. Pass. Now try #777777 on #ffffff. The ratio is 4.48:1. Fail.

The colors are #767676 and #777777. One integer difference per channel. Nobody on earth can see the difference between those grays on a white background. But one passes WCAG AA and the other doesn't, and that difference is the whole thing separating "your product is accessible" from "you're getting a demand letter."

The reason is that the formula is continuous but the compliance bar is sharp. The ratio drops smoothly as you make the gray lighter; at some point it crosses 4.5, and the WCAG spec says "below this number, fail." Perceptually, the experience is gradual. At 4.5:1 the text looks slightly faded. At 4.0:1 it looks noticeably light. At 3.0:1 it's genuinely hard to read. But WCAG has no "slightly faded" bucket — everything that isn't 4.5 or greater is marked failing, and everything 4.5 or greater passes.

This is a known problem with the 2.1 formula and one of the things APCA is specifically designed to fix. For now, the practical workaround is: if you're near the edge, go darker. #595959 (contrast 7:1) is your safe "darkest light gray." Anything darker definitely passes AA and AAA, and you never have to argue with a checker again.

Six real debugging cases

These are the patterns I hit constantly when auditing existing products. If you're here because your audit came back red, one of these is probably the cause.

1. Gray fails on white but passes on off-white

This is the classic "wait, WHAT?" case. You have text at #767676:

  • On #ffffff → 4.54:1 — pass
  • On #f5f5f5 → 4.11:1 — fail
  • On #fafafa → 4.31:1 — fail
  • On #efefef → 3.91:1 — fail

Same text, same gray, slightly different background, and it goes from passing to failing. This feels wrong but is mathematical. The luminance formula is non-linear — small changes near the top of the brightness range (whites) affect the ratio more than small changes at the bottom. When you swap pure white for a slightly off-white card background, the numerator in the ratio formula drops noticeably. The darker text-gray numerator barely changes. Net result: the ratio drops.

The practical fix: if you're using a light-gray card or panel background, the text inside needs to be darker than the text on your white page background. Test each text-on-surface combination individually with our Color Contrast Checker — you can't assume a gray that passes on white also passes on your almost-white card. This is also why design systems that define "surface" and "background" colors tend to end up defining multiple body-text colors for each surface.

2. Yellow on white fails, even though it "looks bright"

#FFFF00 (pure yellow) on #FFFFFF (pure white) is about 1.07:1. Utterly failing. But yellow feels like a bright, attention-grabbing color, so why?

Because luminance has that weight distribution: 0.2126 red + 0.7152 green + 0.0722 blue. Pure yellow is full red plus full green plus zero blue. Its relative luminance is 0.2126 + 0.7152 = 0.9278. White is 1.0. The two colors are nearly the same luminance — one's just "tinted" yellow — so the contrast is almost nothing.

The same thing happens with pastel colors, bright cyan, lemon, most "warm" highlights. If your branding is yellow text on white backgrounds, you have two options: darken the yellow toward amber/mustard until you get contrast, or pair it with a dark background. Yellow on black is 20:1 and works beautifully. If you need to keep the yellow hue roughly intact, the Color Shades Generator can produce a scale from your base color so you can pick the darkest yellow that still reads as yellow.

3. Blue link on dark-gray body text

Modern design trend: body text at #333333, links at #3366ff or similar blue. Both pass contrast against the white background. #3366ff on #ffffff is 5.04:1 — pass. #333333 on #ffffff is 12.63:1 — pass. Done, right?

WCAG 2.1 SC 1.4.1 (Use of Color) and SC 1.4.11 (Non-text Contrast) say you also need to distinguish the link from surrounding text in some way other than color alone (for color-blind users), or make the link visually obvious some other way. The practical reading: if your only differentiator between body text and link text is the color, you also want the link color to have a noticeable contrast from the body text color, not just from the background.

#3366ff vs #333333 is 1.97:1. A person with low vision might not be able to distinguish the two by color alone. The remedy is usually: keep an underline on links. WCAG doesn't require underlines, but it does require that color-alone doesn't carry meaning. Underlines are the cheapest way to satisfy that. Your blue can then be any passing value.

If you want to check whether a color combination is distinguishable for color-blind users, our Color Blindness Simulator runs your page through the three main color vision deficiency filters and shows you what your links look like to someone with deuteranopia or protanopia. Worth doing once per design system.

4. Placeholder text that's always too light

Placeholder text is in an awkward spot. WCAG 2.1 doesn't strictly require 4.5:1 for placeholder, because placeholder is "instructional" rather than "informational" — the information is still available in the label. But WCAG 2.2 SC 3.3.2 (Labels or Instructions) and 1.4.13 (Content on Hover or Focus) strongly imply placeholder should be readable, and case law has trended toward treating placeholder as needing to meet contrast.

In practice: if the placeholder is the only thing telling the user what to enter, it needs 4.5:1. If there's a persistent visible label above the input, placeholder can be lighter (3:1 is still defensible). Many teams decide not to have that argument and just make placeholder hit 4.5:1 — usually something like #595959 on #ffffff — and use the ::placeholder CSS selector to style it. The result looks darker than designers expect, but it's genuinely more usable and keeps you out of the gray zone.

5. The disabled button that's technically fine but hostile

WCAG 2.1 says disabled controls don't need to pass contrast. Every contrast checker will give your disabled button a pass. This is technically correct and practically terrible.

The problem: disabled buttons still convey meaning ("this action exists but isn't available right now"). A disabled Save button in your form needs to be distinguishable as the Save button. If it's a pale gray shape with pale gray text on a white background, a user with low vision might not see it at all, which is functionally identical to not having the button. They can't even know there's an action they need to enable.

The fix: disabled buttons need to be visible as buttons. The text-vs-button-background contrast can be low (WCAG allows this), but the button itself should have some outline or tone that distinguishes it from the page background. We often use a #bbbbbb border on disabled buttons plus the lighter fill — passes the spirit of the rule without passing WCAG's letter for the text. Or redesign so the button only shows up when it's actionable.

6. Branded colors that no passing combo will save

Occasionally a brand has a red CTA and an orange background they refuse to change. You'll try every permutation:

  • Red #ff2d55 on orange #ff9900 → 2.08:1, fail
  • Red #ff2d55 on white #ffffff → 3.61:1, fail for body but pass for large text
  • Red #c70039 on white #ffffff → 5.89:1, pass
  • White #ffffff on red #c70039 → 5.89:1, pass

The only winning move is leave the brand system and pick a passing combo. When that's not politically possible, the escape hatches are:

  • Make the text larger, so it only needs to hit 3:1.
  • Add a thick border or underline so the text isn't the only visual anchor.
  • Accept AAA exemption for large-scale text (titles, logos) but fix the body.

If you want to preview what a darker version of the brand red looks like before pitching it to marketing, the Color Shades Generator produces a 10-step scale from a base color. Pick the darkest shade that still reads as "red" — usually one or two steps darker than the original is enough.

Dark mode has its own failure mode

Everything above assumed light mode. Dark mode has different physics and different problems.

Pure white text (#ffffff) on pure black (#000000) has a 21:1 contrast ratio. The maximum possible. WCAG says this is the best you can do. Humans with low vision often describe reading that combination as painful. The phenomenon is called halation — on emissive displays like OLED, bright pixels on a dark background appear to "bleed" outward because of how the human eye resolves high-dynamic-range edges. Text gets fuzzy-looking. Extended reading causes eye strain.

What works better:

  • Off-white text like #e0e0e0 or #d0d0d0, not pure white
  • Dark gray background like #121212 or #1a1a1a, not pure black
  • Contrast ratio of 12:1 to 16:1, not 21:1

This matches Google's Material Design and Apple's HIG recommendations for dark mode. It also, interestingly, still passes WCAG AAA easily — #e0e0e0 on #1a1a1a is 13.3:1. You can have better dark mode ergonomics and better WCAG scores by leaving pure black and pure white alone.

The other dark-mode gotcha: saturated colors (especially blues and purples) can become illegible on dark backgrounds even when the ratio passes. #5c6bc0 on #1a1a1a is 4.8:1 — passes AA. But the blue has so little luminance range from the dark background that the eye can't "lock onto" it. Desaturate or lighten the accent, not just ratio-check it. Rules alone won't catch this.

For dark-mode palettes, I generally build two parallel sets of the design system tokens and test both. The Color Palette Generator can help you generate a complementary-harmony palette that you then check in both modes.

What APCA fixes — and when to actually use it

APCA stands for Advanced Perceptual Contrast Algorithm. It's the work of Andrew Somers and has been under active development as a candidate for WCAG 3 (also known as "Silver") for several years. If you're hearing about it for the first time: yes, the people who built WCAG know there are problems with the 2.1 contrast formula, and APCA is their answer.

The practical differences:

  • APCA outputs "Lc" values, roughly -108 to 108, instead of ratio. You interpret them: Lc 75+ is fine for body text, Lc 60 for large text, Lc 45 for very large / non-text. Negative values mean light text on dark background (APCA explicitly handles dark mode differently, where WCAG does not).
  • APCA is polarity-aware. Dark text on a light background works differently for the human eye than light text on a dark background. WCAG treats them symmetrically (4.5:1 is 4.5:1 either way). APCA doesn't. This is one of the big fixes.
  • APCA uses different luminance math. Specifically, it accounts for font weight and size, ambient contrast, and some perceptual quirks that the 1970s BT.709 coefficients miss.
  • APCA is more permissive in some cases, more strict in others. A design that passes WCAG 2.1 might fail APCA (e.g., light blue text on white, which WCAG rates more generously than APCA does). A design that fails WCAG 2.1 might pass APCA (e.g., some of the #767676-on-white edge cases we looked at earlier).

Here's how you can think about it. WCAG 2.1 is a minimum floor, set conservatively, using math that was never quite right for modern screens. APCA is a more nuanced model that's closer to what humans actually perceive. APCA is the future of accessibility contrast math, but it's not normative yet — WCAG 2.1 and 2.2 are still the standards you'll be audited against.

The practical guidance right now (May 2026):

  1. Design to WCAG 2.2 as the floor. That's what the audit tools test. That's what lawsuits cite.
  2. Also design for Lc 75+ on body text. If your text passes APCA at Lc 75, it's legitimately comfortable to read, not just ratio-compliant.
  3. When APCA and WCAG disagree, lean toward whichever is stricter. You won't regret being more readable.
  4. Watch WCAG 3 progress. It's in draft. The migration path is likely to be gradual and backward-compatible.

There's a library called bridge-pca that does exactly that — it's a simplified APCA implementation that's guaranteed backward-compatible with WCAG 2. If bridge-pca passes a combination, WCAG 2 passes it. This is a decent option if you want to start moving toward APCA-style design now without breaking audits. Not all design systems need this yet, but large-scale accessibility-critical products (government, healthcare, finance) are starting to adopt it.

If you want to test your combinations under APCA, apca-w3 is the canonical reference implementation. It's MIT-licensed and runs in the browser.

A debugging workflow that actually works

When you have an accessibility audit with contrast failures, here's the workflow I use:

1. Reproduce the ratio yourself. Don't trust the auditor. Grab the reported colors, paste them into our Color Contrast Checker or any checker you prefer, and confirm the fail. Sometimes audit tools flag things that look odd — a background that's behind a semi-transparent overlay, for example, which changes the effective color. If you can't reproduce the fail, the auditor is probably measuring something you didn't design.

2. Inspect the real page, not just the design file. Tailwind, user CSS overrides, system dark mode preferences, browser zoom — any of these can shift actual rendered colors. Open the page in Chrome DevTools, pick the failing element, and confirm the computed color and background-color. The Color Picker is useful for sampling any arbitrary pixel if the auditor didn't tell you exactly which color is which.

3. Decide: darker text, lighter background, or both? Usually one change is enough. If the text currently passes on some backgrounds and fails on others, it's the background-family you need to address, not the text color. If the text fails everywhere, darken the text.

4. Use a scale, not a guess. Generate a 10-step shade scale from your current failing color with the Color Shades Generator, then pick the darkest step that still reads as the intended color family. Don't just eyeball a "darker version" — scales are cheaper than arguing with designers.

5. Test under color blindness simulation. Before you ship the fix, run the page through the Color Blindness Simulator. If a color-blind user can't distinguish two elements, contrast ratio won't save you.

6. Check both modes. If your site has dark mode, run the audit there too. Dark-mode contrast is fragile in ways that often only show up in actual use.

7. Document the failing token in your design system, so you don't repeat the fix in three other places. This is where a proper token-based color system pays for itself.

Most "contrast problems" aren't really problems in individual components. They're symptoms that your design system has a weak layer of tokens, or that your team relied on ad-hoc color picking instead of a scale. If you're building a color system from scratch or auditing an existing one, we covered that in Build a color system that actually works for developers. It's a good companion to this post.

Frequently asked questions

Q: Why does #777 gray fail on white but #767 pass? Because the luminance formula is non-linear near the top of the brightness range. #767676 has luminance 0.2086; #777777 has luminance 0.2105. That 0.002 difference flips the contrast ratio from 4.54 (pass) to 4.48 (fail). You can't see it; the formula cares. The honest fix is to go darker than the edge — #595959 or darker passes comfortably.

Q: What's the difference between AA and AAA? AA is the practical bar most audits use: 4.5:1 for normal text, 3:1 for large text. AAA is the enhanced level: 7:1 for normal, 4.5:1 for large. AAA is aspirational — most of the web doesn't hit it, and WCAG 2.1 explicitly says "achieving AAA for all content is not possible for some content types." For most commercial products AA is the target; AAA is for critical content (medical info, legal disclosures, government services).

Q: Is 3:1 enough for large text? What counts as large? Large text means 18 point or larger, or 14 point bold or larger. At 96 DPI, 18 points is 24 CSS pixels; 14 points is about 18.7 CSS pixels. Most H1/H2 headings qualify. Body text almost never does. If in doubt, measure the rendered size — not the design file font size.

Q: Do icons and UI components need to pass contrast? WCAG 2.1 SC 1.4.11 says non-text content (icons, UI components, graphical information) needs 3:1 against adjacent colors. This includes active states of form controls, focus indicators, checkbox states, and meaningful graphics. It does not include decorative icons or UI elements inactive states.

Q: Does placeholder text need to pass contrast? Technically the 2.1 spec is ambiguous. In practice: if placeholder is the only thing telling the user what to enter, it needs 4.5:1. If there's a persistent label, 3:1 is defensible. Most teams just make placeholder hit 4.5:1 and move on, which is the safest interpretation and matches current legal trend.

Q: How do I pick a passing gray? Bookmark #595959 — that's the lightest gray that passes 7:1 (AAA) on pure white. If you need a passing gray on white, and you start lighter than #595959, you're gambling. #767676 passes AA on white but only barely, and fails on any off-white.

Q: Why does yellow fail on white? Luminance math weights green heavily and red moderately. Yellow = red + green, both at full, so it's almost as luminous as white. Pure yellow on pure white is about 1.07:1. Yellow on a dark background (especially black) scores around 20:1 and works great. If you must use yellow with white, darken toward amber.

Q: What is APCA and when should I switch? APCA is a proposed replacement for the WCAG 2.1 contrast formula, more accurate to human perception, polarity-aware (dark mode works differently than light mode), and intended for WCAG 3. It's not normative yet. Right now, design to WCAG 2.2 as the floor and use APCA as a secondary sanity check. Products that care deeply about dark-mode ergonomics or serve low-vision users might want to adopt it earlier via bridge-pca.

Q: Is dark mode harder to make accessible? Not harder, but different. Pure white on pure black scores 21:1 but causes halation. Off-white on dark gray (e.g., #e0e0e0 on #1a1a1a, 13.3:1) reads more comfortably. The contrast formula doesn't know about halation. Also saturated accent colors (blues, purples) can become hard to read on dark even when they ratio-pass. Test in both modes with actual users.

Q: Do hover states need contrast? The hover state itself doesn't need to pass (it's not the default state), but any information conveyed by the hover state — like "this button is now focused" — does need to be visible to keyboard users. Focus indicators specifically must meet 3:1 against the component's default state per WCAG 2.1 SC 1.4.11.

Q: Why do two colors that look different have the same contrast ratio? Contrast ratio is a 1-dimensional output (luminance ratio) of a 3-dimensional input (R, G, B). You can have many different color pairs with the same luminance ratio. This is another case where the formula simplifies reality — two colors can have identical ratio and very different perceived readability.

Q: How do I test contrast in Figma, Chrome DevTools, or my existing design? Figma plugins: Stark (free tier), Able, Contrast. Chrome DevTools has built-in contrast check in the element panel (click any text → go to Styles → look for the contrast icon next to the color). Or use a quick online checker like ours with copy-paste. For batch testing whole pages, axe DevTools or WAVE will flag all contrast failures at once.

Q: Can text on images pass WCAG? Only if the image is decorative (WCAG gives an explicit exemption for text over decorative images). If the text is meaningful (button labels, hero headings), the WCAG requirement is that the text has sufficient contrast against whatever is behind it — and since an image varies pixel by pixel, this is impossible to guarantee without either (a) a semi-transparent overlay that brings the background luminance under control or (b) a text shadow that creates an artificial high-contrast outline. Both are common patterns; both are acceptable.

Q: What happens if my site fails WCAG — can I get sued? In the US, yes. ADA lawsuits citing WCAG 2.1 AA as the standard have been filed against thousands of companies in the past five years. A demand letter typically cites specific failures (e.g., "body text #999 on #fff, ratio 2.85:1, fails AA"). Settlement or remediation is usually cheaper than litigation. Outside the US, the regulatory environment varies — EU is moving toward mandatory WCAG 2.1 AA compliance for public sector sites via the European Accessibility Act (effective 2025).

One more thing

I've been auditing contrast for a decade and the thing I've learned is that the WCAG number is a floor, not a ceiling. The formula's approximations are conservative — if you pass 4.5:1 you're usually fine, and if you pass 7:1 you almost never have contrast complaints. But nothing about the ratio tells you "your users actually enjoy reading your content." That's a design question, not a compliance question.

If your audience includes anyone with vision impairment (yours does — 1 in 12 men and 1 in 200 women have some form of color vision deficiency, and more broadly, aging populations universally lose contrast sensitivity), design past the minimum. Use higher ratios for body text. Adopt an APCA-leaning design system if your team has the capacity. Test in real browsers with real zoom levels and real ambient lighting. Ask people who actually rely on this for feedback.

And when in doubt, the tools make this fast. Color Contrast Checker for quick pair-testing. Color Blindness Simulator to see what color-blind users see. Color Shades Generator for systematic scale-based fixes. Color Converter for format changes. They all run in your browser, no signup, no uploads.

Honest math and real users matter more than compliance certificates. Ship for humans.

If you're thinking about canonical URLs or other SEO issues next, we wrote about the most common canonical tag mistakes. And for teams building design systems: Build a color system that actually works for developers. Both companion posts to this one.

wcagcolor-contrastaccessibilitya11ycontrast-ratioapcawcag-3wcag-2.2ui-designdark-modecssweb-developmentdesign-systemsinclusive-designfrontendluminance
Advertisement

Practice with free tools

200+ free developer tools that run in your browser.

Browse all tools →