You're wrapping up a UI review when someone flags the placeholder text. "Does that gray pass WCAG?" You eyeball it. Looks fine to you. But running it through a WCAG color contrast checker tells a different story — 2.3:1 ratio. Fails AA by a wide margin. And you're not alone in missing it; about 300 million people have some form of color vision deficiency that makes low-contrast text genuinely hard to read.
What is a contrast ratio?
A contrast ratio is a number representing the difference in relative luminance between two colors. Pure white on pure black gives you 21:1 — the theoretical maximum. Two identical colors give a ratio of 1:1, the worst possible score. Most real-world text-on-background combinations land somewhere in between, and the goal is to stay above the AA threshold.
Accessible color ratio — AA vs AAA
- AA — Minimum for compliance. Normal text needs 4.5:1. Large text (18px+ or 14px+ bold) gets a lower bar at 3:1.
- AAA — Enhanced accessibility. Normal text needs 7:1. Large text needs 4.5:1.
For most products, AA is the realistic target. AAA for normal body text is genuinely hard to achieve while keeping a flexible visual design — you're essentially limited to near-black or near-white.
Common contrast failures worth auditing
- Gray placeholder text on white — often lands around 2.8:1, which fails AA
- Light blue links on white — Tailwind's
blue-500on white is 3.0:1, failing AA for normal text - Disabled button states — "unclickable" shouldn't also mean "unreadable"
- White text over a light gradient or image background — fails at the bright end of the gradient
Color accessibility audit in seconds
The onHover Color Eyedropper — part of the Chrome extension — shows the WCAG contrast ratio as you pick colors. Sample the text color, then the background — the ratio and AA/AAA pass/fail grade appear immediately. No copy-pasting hex codes into a separate tool.
Design tokens that reliably pass
When building your color system, pre-validate text and background token pairs. These combinations clear AA without sacrificing too much design flexibility:
slate-200 on slate-900— 12.6:1, passes AAAwhite on blue-600— 4.6:1, passes AAslate-900 on amber-400— 8.2:1, passes AAA
Color contrast isn't just an accessibility compliance checkbox to tick before launch. If your text is straining for someone with deuteranopia, it's probably also straining for everyone reading on a phone screen in direct daylight. Treating contrast as a quality signal — not just a legal requirement — is the right way to think about it.
Types of color vision deficiency that affect your users
The "300 million people" figure gets cited often, but it covers several distinct conditions, not one. Understanding the differences matters because they affect different parts of your UI in different ways.
Deuteranopia is red-green color blindness and the most common variant — roughly 6% of men have it. Greens and reds look similar. Status systems that rely on red-for-error and green-for-success without additional differentiation (icon, label, position) are the classic failure mode here.
Protanopia is red weakness — different from deuteranopia but in the same red-green spectrum. About 1% of men. The practical impact on UI is similar: color-coded status systems that don't pair color with a second signal fail for these users.
Tritanopia is blue-yellow deficiency. Rarer, but worth knowing if your brand is blue-heavy. Blue text on white and blue CTAs are specifically affected — and if your design system is built around a blue primary color (as many are), tritanopia is the one to test explicitly.
Achromatopsia — complete color blindness — is rare, but those users rely entirely on luminance contrast. No color differentiation at all. This is why the WCAG formula is luminance-based: a ratio that passes for standard vision passes for achromatopsia too, because it's measuring luminance difference rather than color difference. Contrast is the universal solution across all deficiency types. That's not a coincidence; it's why the spec is designed the way it is.
Testing contrast in dark mode
Dark mode adds a layer of complexity that most teams forget to check separately. The light mode audit passes, dark mode ships, and suddenly there are contrast failures nobody tested because everyone assumed dark mode was "just the inverted version."
White-on-slate-900 at 15:1 in dark mode? Not the concern. The concern is your semantic colors — status indicators, alert banners, badge text. Red-500 on white (light mode) has a different contrast ratio than red-500 on slate-900 (dark mode), because the luminance relationship changes depending on which background you're measuring against. Red is relatively bright on its own, which means it passes against dark backgrounds more easily than light ones. Green is the opposite — green-500 on white is fine, but green-500 on a very dark surface can pass or fail depending on the exact shade.
The specific colors to audit in dark mode:
- Error red on dark card backgrounds
- Success green on dark surfaces
- Warning amber on dark backgrounds (amber tends to be bright enough to pass, but check anyway)
- Muted/secondary text — the "lighter" shade of body text that works well at slate-400 on white but might not meet AA on dark surfaces
The onHover Color Eyedropper works on dark mode interfaces exactly the same way as light mode. Toggle dark mode on your product, open the onHover Chrome extension, sample the text and background colors directly from the rendered dark mode UI, and get the ratio. Run this as a separate pass from your light mode audit — don't assume the same token pairs behave the same way in both themes.
Dark mode audit checklist
Check every semantic color token pair in dark mode separately from light mode. Status colors (error, success, warning, info) are the highest risk — they're often defined as single values that weren't validated against dark surfaces during the token design phase.
Building an accessible color palette from the start
The most efficient approach is to validate color pairs when building the token system — not after the UI is built. After-the-fact contrast audits are real work: you find failures, you negotiate shade adjustments with designers, you re-test, you update components. The same work done upfront, at token definition time, is just a few minutes per pair and prevents the whole cycle.
The workflow we use: for each semantic color token pair — text on surface, primary on background, error on card, success on input — we record the contrast ratio next to the token definition. Any pair that doesn't pass AA for its intended use case gets a different shade before the token is adopted. No token pair enters the system without a validated ratio attached.
This validation-at-definition approach means you're never in a position where a designer or developer uses an "approved" token combination that silently fails accessibility. If the token is in the system, it passed. That constraint is the thing worth building into your design process.
Tools that make this easier during design: Accessible Palette (a web tool for building WCAG-validated palettes), the Contrast plugin in Figma, and the built-in accessibility checker in Figma for component-level review. Build the validation into the phase where tokens are created — remove it from the pre-launch checklist entirely by making failures impossible to introduce in the first place.
Automated contrast checking in CI
Manual audits with the onHover developer toolkit catch issues during active development. But accessibility regressions can also be introduced later, when tokens change, when components are updated, or when a third-party library update changes some rendered color value unexpectedly. That's where CI-level automation fills the gap.
Tools like Axe — built into Playwright and Cypress as plugins — can run contrast checks against your rendered components during CI. The workflow is straightforward: run your visual component tests with an accessibility audit at the same time. Contrast failures appear in the same CI results as functional failures. A PR that introduces a contrast regression gets flagged before merge, not after a user reports it.
The realistic scope for CI contrast checking: your component library. Storybook with the Axe addon runs contrast checks on every story. Playwright component tests can run Axe on any rendered component. These cover the cases where regressions are most likely — centralized components used across many pages.
CI automation doesn't replace manual checking with onHover's Color Eyedropper. Automated tools can only check what they can access programmatically — they miss dynamic states like hover, focus, and loading. They miss contrast issues that only appear over image backgrounds. They miss issues in content-driven pages where text color meets unpredictable background images. Manual checking covers those cases. CI automation covers component regressions. Both are part of the same system, doing different things.
If you're a designer who codes or works closely with a dev team, the onHover for Designers page covers the full set of tools built around visual QA and design-to-code handoff — including the Color Eyedropper used throughout this workflow.