Accessible Color Contrast: WCAG AA, AAA & APCA Guide
Make text readable for everyone: WCAG AA/AAA contrast ratios, the new APCA method, non-text contrast and a fast color-pairing workflow.

On this page▾
Accessible color contrast is what separates an interface everyone can read from one that quietly excludes people with low vision, aging eyes, or a phone held in direct sunlight. The rules are more nuanced than a single magic number: there is the familiar WCAG 2 ratio, a separate 3:1 floor for icons and borders, and a newer perceptual method called APCA heading into WCAG 3. This guide covers all of it — and a fast workflow for fixing failing pairs without abandoning your brand.
What contrast ratio actually measures
A contrast ratio compares the relative luminance of two colors — how much light each one reflects, weighted for how sensitive human eyes are to red, green and blue. Green contributes most to perceived brightness, blue the least, which is why a saturated blue can look "dark" even when its RGB numbers are high.
The WCAG formula is (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter color's luminance and L2 the darker one's. That gives a scale from 1:1 (two identical colors, invisible) to 21:1 (pure black on pure white). The 0.05 is a flare constant that models the faint glow of ambient light on a screen — remember it, because it distorts the dark end later.
| Pair | Hex | WCAG ratio | APCA Lc |
|---|---|---|---|
| Black on white | #000000 / #FFFFFF | 21:1 | 106 |
| Dark gray on white | #595959 / #FFFFFF | 7.00:1 | 84 |
| Mid gray on white | #767676 / #FFFFFF | 4.54:1 | 72 |
| Blue-500 on white | #3B82F6 / #FFFFFF | 3.68:1 | 64 |
| Light gray on white | #9CA3AF / #FFFFFF | 2.54:1 | 50 |
| White on white | #FFFFFF / #FFFFFF | 1:1 | 0 |
You never have to compute this by hand — paste any two colors into the contrast checker and it returns the ratio plus every WCAG verdict at once. The number itself is order-independent: swapping text and background gives the identical ratio, a property that turns out to be a feature and a flaw.
WCAG 2: AA vs AAA and the large-text rule
WCAG 2 sets the wcag contrast ratio minimums by conformance level and text size. There are only four numbers to remember:
| Level | Normal text | Large text |
|---|---|---|
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |
The aa vs aaa contrast decision is mostly about ambition. AA is the practical and legal baseline — it is what most accessibility laws (the ADA in the US, the EAA in Europe) effectively reference, so treat 4.5:1 for body text as non-negotiable. AAA (7:1) is worth targeting for long-form reading, medical or financial products, and anywhere your audience skews older. Hitting AAA everywhere often forces near-black text, which can feel heavy, so many teams use AAA for body copy and let AA cover the rest.
What counts as "large"
Large text gets an easier bar because bigger glyphs survive lower contrast. WCAG defines large as 24px (18pt) regular or 18.66px (14pt) bold and up. So an #767676 gray heading clears the 3:1 large-text bar comfortably, while the same gray as 14px body text sits right on the 4.5:1 AA line. Bold weight matters: thin or light font weights need more contrast than the ratio alone suggests, because hairline strokes give the eye less color to work with.
Non-text contrast: icons, borders and focus rings
Text is only half the picture. WCAG 2.1 added success criterion 1.4.11 Non-Text Contrast, which requires 3:1 for any non-decorative interface element a user has to perceive: input borders, toggle states, icon-only buttons, focus rings, and the colored segments of a chart.
This is where most "clean" designs quietly fail. The default 1px borders shipped by popular UI kits are far too faint:
| Border on white | Hex | WCAG ratio | Meets 3:1? |
|---|---|---|---|
| gray-300 | #D1D5DB | 1.47:1 | No |
| gray-400 | #9CA3AF | 2.54:1 | No |
| mid gray | #949494 | 3.03:1 | Just |
| gray-500 | #6B7280 | 4.83:1 | Yes |
A #D1D5DB border on white is invisible to a lot of users at 1.47:1 — it does not even reach half the required floor. You need roughly #949494 or darker before a border, divider or unfocused input outline does its job. Focus rings deserve special attention because they are a keyboard user's only cue: a #2563EB ring clears 3:1 on both white (5.17:1) and a dark #111827 surface (3.43:1), which is why a single saturated accent often works as the focus color in both themes. Icons that carry meaning (a red error triangle, a muted "more" chevron) are held to the same 3:1 standard as text-sized borders.
The limits of WCAG 2 contrast
WCAG 2's ratio is simple and enforceable, but it was reverse-engineered from a 1980s formula and gets human perception wrong in two well-documented ways.
It is polarity-blind. Because the math takes the lighter and darker luminance regardless of which is the text, dark-text-on-light and light-text-on-dark of the "same" ratio score identically — even though the eye reads them differently. #1D4ED8 on white and white on #1D4ED8 both score exactly 6.70:1, yet light text on a dark field blooms and thins in a way dark-on-light does not.
It over-credits mid grays on dark backgrounds. Thanks to that 0.05 flare constant compressing the dark end, a medium gray like #8A8A8A on a near-black #1A1A1A scores 5.04:1 and "passes" AA for normal text — but almost nobody finds that combination comfortable to read as body copy. WCAG 2 also inflates saturated mid-tones: black on orange #FFA500 scores a dramatic 10.63:1, well into AAA, while it reads as merely solid, not exceptional.
None of this means ignore WCAG 2 — it is the standard you are legally measured against, and meeting it prevents the worst failures. But when a pair passes by a hair, or you are designing in dark mode, get a second opinion: APCA.
APCA and WCAG 3: the perceptual method
So what is APCA? It stands for the Accessible Perceptual Contrast Algorithm, the contrast model proposed for the next major standard, WCAG 3. Instead of a ratio it outputs a lightness contrast value (Lc) running from about -108 to 106. Magnitude is what matters; the sign just records polarity — positive for dark text on a light background, negative for light text on dark.
APCA fixes the two flaws above. It is polarity-aware (so those flipped blues score 82 and -87, not the same number), and it weights contrast the way the eye actually responds, so it stops over-rewarding dark-mode grays. As a rough reading of the draft lookup tables:
- Lc 75+ — fine for small or thin body text.
- Lc 60+ — comfortable for normal body text.
- Lc 45+ — large or bold headings only.
- Below ~30 — decorative or disabled states.
| Pair | WCAG | APCA Lc | What it means |
|---|---|---|---|
#8A8A8A on #1A1A1A | 5.04:1 | -38 | WCAG passes AA; APCA says headings only |
#1D4ED8 on #FFFFFF | 6.70:1 | 82 | Comfortable body text |
#FFFFFF on #1D4ED8 | 6.70:1 | -87 | Same WCAG number, different Lc |
That first row is the headline: WCAG 2 green-lights #8A8A8A dark-mode text for paragraphs, while APCA correctly flags it as readable only at large sizes. APCA is still a draft — do not ship it as your sole compliance check — but it is an excellent tiebreaker. The same contrast checker reports the APCA Lc alongside the WCAG verdict so you can see both at a glance.
A fast workflow for accessible pairs
The single most useful habit: when a pair fails, adjust lightness, not hue. Swapping to a different color throws away your brand; darkening the text or lightening the background raises the ratio while keeping the same color identity. Working in HSL (or OKLCH for even spacing) makes this a one-number change — see HEX, RGB and HSL for why HSL is the format to tweak in.
Here is the move in practice. A brand blue like the one on our blue color page — #3B82F6, Tailwind's blue-500 — is a popular button color, but as text on white it manages only 3.68:1: it passes for large headings but fails AA for body copy. Keep the hue, drop the lightness:
| Token | Hex | HSL | WCAG on white | AA body? |
|---|---|---|---|---|
| blue-500 | #3B82F6 | hsl(217, 91%, 60%) | 3.68:1 | Fail |
| blue-600 | #2563EB | hsl(221, 83%, 53%) | 5.17:1 | Pass |
| blue-700 | #1D4ED8 | hsl(224, 76%, 48%) | 6.70:1 | Pass |
/* Keep blue-500 for large fills, switch to a darker step for text */
.btn { background: #3B82F6; color: #ffffff; } /* 3.68:1 — large/bold only */
.link, .label { color: #2563EB; } /* 5.17:1 — passes AA body */
The hue barely moves (217 to 224) while lightness falls from 60% to 48%, so the family still reads as "your blue." Fold that into a repeatable loop:
- Pick a text and background pair.
- Check it in the contrast checker; read both the WCAG ratio and APCA Lc.
- If it fails, lower lightness on the darker color or raise it on the lighter one — never reach for a new hue first.
- Re-test non-text elements (borders, icons, focus rings) against the 3:1 bar.
To build a whole interface this way, generate an evenly stepped ramp with the color palette generator, assemble production tokens with the brand color kit (it surfaces which steps are text-safe), and find an accent that still contrasts with your base using the complementary color finder. For the bigger picture on harmonies and roles, color schemes explained is the companion read.
Designing for color blindness
Contrast ratios assume the viewer sees color the way you do — about 1 in 12 men and 1 in 200 women do not. The most common form, red-green deficiency, is why color blindness design has one iron rule: never rely on hue alone to convey meaning.
Consider the classic success-green / error-red pairing. Tailwind's red-600 #DC2626 and green-600 #16A34A look obviously different to most people, but their luminance is nearly identical — the contrast between them is just 1.47:1. Strip the hue (in grayscale, or for someone with deuteranopia) and they collapse into the same muddy mid-tone. The red drifts toward olive, the green toward a grayish purple, and the "stop versus go" signal is gone.
The fix is redundant encoding — pair every color cue with a second one:
- Add an icon or shape: a check for success, a triangle for error.
- Add text or a label, not just a colored dot.
- Use patterns or position in charts, plus direct labels instead of a color-only legend.
- Underline links in body text rather than signalling them with color alone.
These cues also help everyone in a hurry or on a bad screen — and keeping that 3:1 non-text floor means the shapes themselves stay visible.
Dark mode contrast considerations
Dark mode is not just "invert everything." The instinct to put pure white #FFFFFF on pure black #000000 produces the maximum 21:1 ratio — and it is too much. High contrast at that extreme causes halation, where light text appears to bloom and vibrate against the black, especially for readers with astigmatism. Pull both ends in:
| Text on background | Hex | WCAG ratio | Role |
|---|---|---|---|
| Pure white on black | #FFFFFF / #000000 | 21:1 | Too harsh — avoid |
| Off-white on charcoal | #E0E0E0 / #121212 | 14.19:1 | Recommended body text |
| Muted gray on charcoal | #A1A1AA / #18181B | 6.91:1 | Secondary text, still AA |
| Light blue link on slate | #60A5FA / #111827 | 6.98:1 | Lightened accent |
Two rules carry most of the weight. First, lighten accents in dark mode: a blue that was legible on white turns muddy on a dark surface, so links and buttons usually shift one or two steps lighter (blue-600 toward blue-400). Second, muted text still has to pass — a "subtle" gray that drops below 4.5:1 fails just as it would in light mode, so verify secondary and placeholder colors, not only primary text. For a complete dark token system, including how to derive a balanced near-black surface, see the dark mode color palette guide. Always test both themes; a token that passes in one can quietly fail in the other.
Frequently asked questions
What is a good color contrast ratio?
For normal body text aim for at least 4.5:1, which meets WCAG 2 level AA. Large text (24px regular or 18.66px bold) only needs 3:1, and 7:1 meets the stricter AAA level. Black on white is the maximum at 21:1.
What's the difference between WCAG AA and AAA?
AA is the common legal baseline: 4.5:1 for normal text and 3:1 for large text. AAA is enhanced: 7:1 for normal text and 4.5:1 for large text. Most teams target AA everywhere and reserve AAA for long-form reading or accessibility-critical products.
What is APCA?
APCA (Accessible Perceptual Contrast Algorithm) is the contrast method proposed for WCAG 3. Instead of one ratio it reports a lightness-contrast value (Lc) from about -108 to 106 that accounts for text polarity, size and weight, so it predicts real readability better than WCAG 2. It is still a draft, so WCAG 2 remains the standard you must meet.
Does contrast apply to icons and borders?
Yes. WCAG 2.1 success criterion 1.4.11 requires a 3:1 ratio for meaningful non-text elements such as icons, input borders, focus rings and chart segments. A faint 1px gray border like #D1D5DB on white (1.47:1) fails; you need roughly #949494 or darker to clear 3:1.
How do I fix failing contrast without changing my brand color?
Adjust lightness, not hue. Darkening the text or lightening its background raises the ratio while keeping the same color identity. For example blue-500 (#3B82F6) on white is only 3.68:1, but dropping to blue-600 (#2563EB) reaches 5.17:1 and passes AA with the hue essentially unchanged.
