DEV Community

Snappy Tools
Snappy Tools

Posted on • Originally published at snappytools.app

Color Contrast in Web Design: WCAG AA, AAA, and Why It Actually Matters

You've probably seen accessibility issues filed against your app for "insufficient color contrast." Maybe you fixed the obvious cases — dark grey on black, light yellow on white — and called it done.

But contrast is more nuanced than that, and the rules behind it directly affect how usable your UI is for a much larger group of people than most developers realise.

Here's what WCAG contrast levels actually mean, how the math works, and how to use them in practice.


The Core Problem: Relative Luminance

Human eyes don't perceive colour linearly. We're far more sensitive to changes in the darker end of the brightness scale than the bright end.

WCAG defines contrast using relative luminance — a value between 0 (pure black) and 1 (pure white) that accounts for this non-linearity.

For any colour like #3d7c52 (our brand green), the formula:

  1. Converts each RGB channel to a linear value (gamma correction)
  2. Weights them by perceived brightness: 0.2126R + 0.7152G + 0.0722B

The contrast ratio between two colours is then:

contrast = (lighter + 0.05) / (darker + 0.05)
Enter fullscreen mode Exit fullscreen mode

The + 0.05 term prevents division-by-zero and compresses the scale at the extremes. Pure black on pure white gives exactly 21:1 — the maximum possible.


WCAG Levels: AA vs AAA

WCAG 2.1 defines three levels of conformance. Contrast requirements differ for normal text and large text (18pt+ or 14pt+ bold):

Level Normal Text Large Text
AA 4.5:1 3:1
AAA 7:1 4.5:1

AA is the minimum legal threshold in many jurisdictions (UK Equality Act, US Section 508, EU EN 301 549). AAA is the ideal for critical content.

There's also a rule for UI components and graphical objects: borders, icons, and interactive element states must meet 3:1 against adjacent colours — a requirement many dashboards fail silently.


Why 4.5:1?

The 4.5:1 threshold was chosen to represent an approximately 20/40 vision impairment — about the level of vision a person with moderate low vision might have, or what an average person in their 70s experiences.

That's a wider audience than most teams acknowledge. About 8% of males have some form of colour vision deficiency. Low-contrast UI also fails in bright sunlight on mobile, on OLED screens with glare, and on low-end displays with poor calibration.


Common Patterns That Fail

Placeholder text

Default browser placeholder text (#767676 on white) hits exactly 4.48:1 — just under AA. It's one of the most commonly failed tests and it's subtle enough to look fine at a glance.

Fix: use color: #595959 for placeholders.

Disabled state text

Many designs use opacity: 0.4 on disabled form fields. A button with green text (#2f855a) at 40% opacity on white will calculate near 1.5:1 — catastrophically low.

Fix: give disabled states an explicit colour that still meets 3:1, even if visually muted.

Hover state changes

Accessibility audits check the default state, but many interactive elements have hover colours that introduce contrast failures. Test hover states explicitly, not just the default.

Coloured text on coloured backgrounds

Dark blue text (#1a365d) on a light blue background (#bee3f8) looks fine — but hits around 3.2:1, failing AA for normal text.


Checking Contrast Without Guessing

The manual formula is tedious. In practice you want a tool you can paste hex codes into and get an instant result.

The Color Contrast Checker at SnappyTools does exactly this — enter two colours, see the contrast ratio, and get an instant AA/AAA pass/fail with a live preview of how the combination actually looks. No signup, no installation.

For larger audits, browser DevTools have a built-in contrast indicator when you inspect text elements. Lighthouse will flag WCAG contrast failures in its accessibility report.


Practical Colour Palettes That Pass

Getting to 4.5:1 against white is easier than it looks if you design for it from the start:

  • Text: #1a202c (near-black) — 17.2:1 on white
  • Secondary text: #4a5568 — 7.0:1 on white (AAA)
  • Muted labels: #718096 — 4.6:1 on white (AA passes, barely)
  • Links: #2b6cb0 (blue) — 4.7:1 on white
  • Success green: #276749 — 5.1:1 on white (don't use #38a169 — it's only 2.9:1)
  • Error red: #c53030 — 5.6:1 on white (#e53e3e fails at 3.1:1)

Notice how the "nice looking" lighter variants almost always fail and the darker alternatives pass. If you build your palette around AAA-compliant base colours and use opacity only for non-text decoration, you'll avoid most contrast bugs before they're filed.


WCAG 3 and APCA

A newer model called APCA (Advanced Perceptual Contrast Algorithm) is being developed for WCAG 3. It's more perceptually accurate — particularly for light text on dark backgrounds, where the 4.5:1 ratio historically underestimates the difficulty.

APCA scores aren't ratios — they're "lightness contrast" values. A score of 75 Lc is roughly equivalent to WCAG AA for body text. The model is still in draft, so WCAG 2.1 AA remains the compliance standard for now.


Quick Checklist

Before shipping any UI with colour:

  • [ ] Body text (16px+): 4.5:1 against background
  • [ ] Headings (24px+ or 18.7px bold): 3:1
  • [ ] Placeholder text: 4.5:1 (not the browser default)
  • [ ] Disabled elements: 3:1 (not just low opacity)
  • [ ] Hover/focus states: same thresholds as default
  • [ ] Icons that convey meaning: 3:1 against adjacent colour
  • [ ] Error/success state text: 4.5:1

Test combinations early. Retrofitting contrast into a finished design is expensive — rethinking your palette is cheap when you're still in Figma.


Colour contrast is one of those accessibility requirements that also makes your UI better for everyone in suboptimal conditions. It's not just about compliance — it's about building something that works.

If you're checking combinations interactively, snappytools.app/color-contrast-checker/ gives you instant ratio feedback with both the visual preview and the WCAG pass/fail.

Top comments (0)