Skip to content

Stop Auditing Contrast After the Fact, Bake It Into Tokens

How to bake contrast and accessibility directly into design tokens instead of catching failures in a late-stage audit, with real token examples.

· · 5 min read
Wall covered in assorted colorful paint samples and swatches used to test color combinations

I once inherited a design system where every color combination technically had a documented hex value, and about a third of them failed contrast when I actually ran the numbers. Nobody had lied. Nobody had cut corners on purpose. The tokens just described colors, not relationships, so nothing stopped a designer from pairing a light gray label with an off-white card and calling it done.

That's the pattern I keep running into. Teams treat accessibility as a check you run at the end, a Lighthouse pass before launch, instead of a property the color system enforces from the start. Fixing it after the fact means re-touching every screen that used the failing pair. Fixing it at the token level means it can't happen again.

TL;DR: Low-contrast text still shows up on 83.9% of home pages, the single most common accessibility failure measured (WebAIM Million 2026). Bake validated contrast pairs into design tokens themselves, and that failure becomes structurally impossible instead of something you catch in a late audit.

Photo by Taylor Heery on Unsplash

Why Do Contrast Failures Keep Slipping Through, Even With a Design System?

Because most token systems store colors, not color relationships. A token named color.gray.400 tells you a hex value. It doesn't tell you where that gray is safe to use as text, or on what background it silently fails 4.5:1. Every designer and developer has to independently remember that constraint, or run a contrast checker manually, every single time. Multiply that across a fifty-screen product and a two-year timeline, and someone eventually forgets. It's not a discipline problem. It's a system that never encoded the rule in the first place.

I'll say the unpopular part out loud: most "accessible design systems" I've audited aren't actually accessible systems. They're regular design systems with an accessibility statement bolted onto the README. The tokens themselves carry zero enforcement.

The thresholds themselves are the easy part. They've been stable since WCAG 2.1, and they're what your tokens need to encode:

ContentWCAG levelMinimum ratioSuccess criterion
Body text under 18pt (or 14pt bold)AA4.5:11.4.3 Contrast (Minimum)
Large text, 18pt+ or 14pt+ boldAA3:11.4.3 Contrast (Minimum)
Body text under 18ptAAA7:11.4.6 Contrast (Enhanced)
Large text, 18pt+ or 14pt+ boldAAA4.5:11.4.6 Contrast (Enhanced)
UI components, focus rings, icons carrying meaningAA3:11.4.11 Non-text Contrast
Disabled controls and pure decorationn/aNo requirement1.4.3, 1.4.11 exceptions

That last row is the one teams get wrong in both directions. Disabled controls are genuinely exempt, so chasing 4.5:1 on a greyed-out button is wasted effort. But a focus ring is not decoration, and it needs 3:1 against whatever sits behind it.

What Does an Accessibility Token Actually Look Like?

Structure it as a pairing, not a single value. Here's a simplified example from a token set I built for a fintech client:

{
  "color": {
    "text": {
      "onLight": { "value": "#1A1A1A", "contrastRatio": "16.1:1", "onBackground": "color.bg.light" },
      "onBrand": { "value": "#FFFFFF", "contrastRatio": "4.87:1", "onBackground": "color.brand.500" },
      "onWarning": { "value": "#1A1200", "contrastRatio": "8.2:1", "onBackground": "color.warning.300" }
    }
  }
}

Every text color token names the background it's validated against and stores the actual ratio it achieved. When a designer picks color.text.onBrand, they aren't choosing a hex value in isolation, they're choosing a relationship that's already been checked. Add a linting step that fails CI if any two paired tokens compute below 4.5:1, and the whole category of bug becomes nearly impossible to ship.

Most "accessible design systems" are just regular design systems with an accessibility statement bolted onto the README.

How Do You Retrofit This Into a System That Already Exists?

Start with an audit, not a rewrite. Pull every color pairing currently in production and run it through a contrast checker, our free WCAG contrast checker does this in bulk if you paste in hex pairs. You'll get a list of failures ranked by how often each pairing appears across your product. Fix the highest-frequency failures first, because that's where the retrofit buys the most coverage per hour spent.

Then, and this is the step most teams skip, don't just fix the failing pair. Add the passing replacement as a named token with its validated background baked in, so the next designer who needs "text on a warning banner" picks from a pre-validated list instead of eyeballing a new hex value that might fail again in six months.

Photo by Mika Baumeister on Unsplash

Is This Worth the Engineering Time It Takes?

Honestly? Yes, and it's cheaper than you'd expect once you've done the audit once. The upfront cost is the token restructuring and the linting rule. The ongoing cost is close to zero, because the system now prevents the mistake instead of requiring a human to catch it every time. Compare that to the alternative: a Lighthouse accessibility score that quietly drops every quarter as new screens ship, followed by a panicked remediation sprint before a compliance deadline. I've run both processes on different projects. The token-first approach is boring in exactly the way good infrastructure should be boring.

There's also a people cost worth naming honestly. Every time a designer has to stop and manually check a contrast ratio, that's a small tax on their attention, paid over and over across hundreds of screens and years of a product's life. Bake the rule into the token and that tax disappears entirely, not reduced, gone. New hires inherit a system that quietly teaches the right habit instead of one that trusts everyone to remember a rule from an onboarding doc they read once. I've watched teams stop debating contrast in design reviews altogether once the tokens did that work for them, which freed up review time for decisions that actually needed a human judgment call. That shift alone changed how my last two clients ran their weekly design critique.

Contrast is one half of accessible type. The other half is choosing typefaces and spacing that don't fight your readers before contrast even enters the picture, which is exactly what our accessible typography guide covers next.

Frequently Asked Questions

What is an accessibility token, exactly?
It's a design token that carries a validated relationship, not just a raw value. A regular color token might be `color.brand.500: #2B6CB0`. An accessibility token pairs that with its approved use: `color.text.on-brand: #FFFFFF` because that specific pairing hits 4.5:1 contrast, verified once, at the token level. The difference sounds small but it changes everything downstream. Nobody picking a text color from your system can accidentally choose a failing combination, because the only options in the token list are ones that already passed. You're not adding new tokens so much as adding metadata and constraints to the ones you already have.
Do I need to support APCA as well as WCAG contrast ratios?
Not yet for most teams, but it's worth tracking. WCAG 2.2's 4.5:1 and 3:1 ratios remain the current legal and practical standard, and that's what auditors and most accessibility overlays check against. APCA, the perceptual contrast model proposed for WCAG 3.0, produces more accurate results for certain color pairs, especially light text on mid-tone backgrounds where the old ratio math gets it wrong. I store both values in my token metadata now, ratio and APCA score, so migrating later is a data lookup instead of a re-audit. WCAG 3.0 is still in draft, so treat APCA as a data point you're collecting, not yet a hard gate.
How do I stop designers from overriding token colors in Figma?
Lock the token, not the designer's judgment. Figma's variable scoping lets you restrict which properties a color variable can apply to, and combined with a component library where text color is bound to a token rather than a free color picker, most accidental overrides disappear. The remaining cases are usually legitimate edge cases, a one-off marketing page, a client override, and those should go through a documented exception process, not a silent hex code swap. I've found that pairing the locked tokens with a Figma plugin that flags contrast failures in real time catches the rest before it ever reaches a pull request.
What is the actual cost of retrofitting accessibility tokens into an existing system?
On a mid-size system, two to three weeks for the token layer itself, longer if your codebase has hardcoded hex values scattered outside the token system. Start with an audit: grep your codebase for hex codes outside your token files, because that raw count tells you the real scope. I did this for a client last year and found 340 hardcoded hex values across their app, most were duplicates of colors already in the token set, just typed by hand instead of referenced. The token work was two weeks. Finding and replacing every hardcoded value took another five, because you can't script your way past manually checking whether a color meant something specific in context.