WCAG Contrast Ratio for Text: 4.5:1 Baseline, Surface Math, and Dark Mode Multipliers
Created 2026-09-03 Β· Updated 2026-09-23 Β· 18 min read
A text color that measures 4.5:1 against white is not guaranteed to pass on a gray-50 card, a blue-tinted alert banner, or a raised dark-mode panel. Contrast is a ratio between two surfaces, and changing the denominator changes the result. Most teams test body text on the page background, see a passing ratio, and ship that token into 12 different UI contexts where it sits on surfaces they never measured.
This guide explains the surface multiplier rule that predicts every ratio drop without cell-by-cell measurement, why muted text needs 5.6:1 on white to stay compliant on tinted cards, why dark-mode text needs 7.7:1 on the base surface to clear three-level elevation stacks, and provides copy-ready CSS tokens tested against the Contrast Checker.
It also corrects an error this page carried since publication. The boundary between βnormalβ and βlargeβ text was printed here as 18px regular / 14px bold. WCAG defines it in points: 18pt and 14pt, which convert to 24px and 18.66px. The old numbers handed the relaxed 3:1 allowance to four sizes that legally require 4.5:1. The corrected boundary, the Tailwind scale mapped against it, and the gray tokens that fail because of it are in the next section.
WCAG 2.2 text contrast requirements
SC 1.4.3 (Contrast - Minimum, Level AA) and SC 1.4.6 (Contrast - Enhanced, Level AAA) set these minimums:
| Text size | AA (Legal minimum) | AAA (Enhanced) | Examples |
|---|---|---|---|
| Normal text <18pt regular (<24px) or <14pt bold (<18.66px) | 4.5:1 | 7:1 | Body copy, labels, captions, helper text, form placeholders, and most subheadings |
| Large text β₯18pt regular (β₯24px) or β₯14pt bold (β₯18.66px) | 3:1 | 4.5:1 | Display headings at 24px+, or bold headings at 18.66px+ |
Source: WCAG 2.2 Success Criteria 1.4.3 and 1.4.6 (W3C Recommendation, October 2023). Enforced by ADA Title III (US), EN 301 549 (EU), AODA (Ontario), and dozens of other jurisdictions.
Correction: this page previously printed the boundary as β18px regular or 14px bold.β
That was wrong, and wrong in the dangerous direction β it granted the relaxed 3:1 allowance to sizes that legally require 4.5:1. WCAG defines large text in points, not pixels. Per W3C Understanding SC 1.4.3: βthe ratio between sizes in points and CSS pixels is 1pt = 1.333px, therefore 14pt and 18pt are equivalent to approximately 18.5px and 24px.β The real boundary is 24px regular and 18.66px bold β a 33% higher bar than the pixel misreading. Anything sized text-lg or text-xl at regular weight needs the full 4.5:1.
The 24px boundary: where the 3:1 allowance actually starts
The pt-to-px slip is the most consequential unit error in color accessibility, because it silently relaxes the requirement on exactly the elements designers most want to lighten: subheadings, section labels, and card titles. Reading the boundary as 18px instead of 24px hands you a 3:1 budget on four sizes that legally need 4.5:1.
Mapped onto the default Tailwind type scale, here is the real requirement per size and weight. The 1.333 conversion means bold text qualifies one full step earlier than regular text:
| Class | Size | Regular weight needs | Bold weight needs | Note |
|---|---|---|---|---|
text-xs | 12px | 4.5:1 | 4.5:1 | Well below both boundaries |
text-sm | 14px | 4.5:1 | 4.5:1 | 14px is not 14pt β common unit slip |
text-base | 16px | 4.5:1 | 4.5:1 | Body copy baseline |
text-lg | 18px | 4.5:1 | 4.5:1 | Danger zone β 0.66px short of bold large |
text-xl | 20px | 4.5:1 | 3:1 | Danger zone at regular weight |
text-2xl | 24px | 3:1 | 3:1 | First size large at regular weight |
text-3xl | 30px | 3:1 | 3:1 | Large |
text-4xl | 36px | 3:1 | 3:1 | Large |
Expressed in other units, with a 16px root: the regular-weight boundary is 1.5rem / 150% and the bold boundary is 1.166rem / 116.6%. If your design system defines type in rem, anything under 1.5rem at regular weight is normal text.
What the misreading costs in practice
The gap between 3:1 and 4.5:1 is not academic β it is roughly 30 points of gray. The lightest neutral gray that clears 3:1 on white is #949494 (3.03:1). The lightest that clears 4.5:1 is #767676 (4.54:1). Believing an 18px subheading only needs 3:1 lets you ship #949494, which is a real 1.5:1 shortfall against its actual requirement.
This is why the *-400 gray step is the single most misused text token in Tailwind-based UIs. Every 400-level neutral fails both thresholds on white β it is not even large-text safe:
| Token | Hex | On white | Large text (3:1) | Normal text (4.5:1) |
|---|---|---|---|---|
| gray-400 | #9CA3AF | 2.5:1 | Fail | Fail |
| slate-400 | #94A3B8 | 2.6:1 | Fail | Fail |
| zinc-400 | #A1A1AA | 2.6:1 | Fail | Fail |
| neutral-400 | #A3A3A3 | 2.5:1 | Fail | Fail |
| gray-500 | #6B7280 | 4.8:1 | Pass | Pass |
| slate-500 | #64748B | 4.8:1 | Pass | Pass |
| zinc-500 | #71717A | 4.8:1 | Pass | Pass |
| neutral-500 | #737373 | 4.7:1 | Pass | Pass |
Every ratio above is recomputed by npm run verify:contrast on each build. Verify any row in the Contrast Checker.
The inversion nobody expects:
The 400-level grays flip completely on dark surfaces. #9CA3AF measures 2.5:1 on white (fails everything) but 7.0:1 on #111827 (AAA). Meanwhile #4B5563 is 7.6:1 on white and collapses to 2.4:1 on the dark base. The 500-level grays are the genuinely treacherous ones: they pass 4.5:1 on white and land at 3.7:1 on dark β large-text-only, and the failure is quiet because the token looks fine in light mode review.
Why you cannot audit this from the stylesheet
Automated contrast scanners read computed size in pixels and apply the threshold correctly. The failure mode is human: a designer hands off βheading, 20px, grayβ with a 3:1 target written into the spec, and the token passes review because the spec itself encodes the wrong boundary. The fix is to put the number in the token layer rather than in review notes.
/* Encode the boundary so the relaxed ratio cannot be applied by accident.
18pt = 24px regular, 14pt = 18.66px bold. Below that, 4.5:1 is required. */
:root {
--text-large-min-regular: 24px; /* 18pt β 1.5rem */
--text-large-min-bold: 18.66px; /* 14pt β 1.166rem */
}
/* Only this class may use a 3:1 gray, and only because it is 24px+. */
.heading-display {
font-size: var(--text-large-min-regular);
color: #949494; /* 3.0:1 on white β valid ONLY at 24px+ regular */
}
/* Everything below the boundary uses the 4.5:1 token, no exceptions. */
.heading-section {
font-size: 20px; /* under 24px β normal text as far as WCAG is concerned */
font-weight: 400;
color: #767676; /* 4.5:1 on white β the lightest compliant neutral */
}
/* Bold crosses the boundary one step earlier, so this is legitimately large. */
.heading-section-bold {
font-size: 20px;
font-weight: 700; /* 20px bold > 18.66px β 3:1 allowed */
color: #949494; /* 3.0:1 on white */
}One caveat on the bold row: WCAG notes that β18 pointβ and βboldβ mean different things in different fonts, and that very thin or unusual typefaces may not qualify even at the nominal size. If your brand font has a light βboldβ (weight 600 in a geometric sans, for instance), treat the bold allowance as unavailable and hold everything to 4.5:1. The 3:1 allowance is a concession to stroke width, not to a number in a CSS file. For the same reasoning applied to component boundaries, see WCAG Contrast Checker for Buttons.
The surface multiplier: why 4.5:1 on white fails on gray-100
Contrast ratio is defined as (L_light + 0.05) / (L_dark + 0.05), where L is relative luminance (0 = black, 1 = white). When you move text from a white background to a tinted surface, you change only the denominator. The text luminance stays the same, so the new ratio is:
= old_ratio Γ (L_white + 0.05) / (L_surface + 0.05)
= old_ratio Γ multiplier
The multiplier is fixed for a given surface and independent of the text color. Measure it once, apply it to every foreground token. Here are the multipliers for common Tailwind surfaces:
| Surface move | Multiplier | Example (4.5:1 token) | Severity |
|---|---|---|---|
| white β gray-50 (#F9FAFB) | Γ0.98 | 4.5:1 β 4.4:1 (still passes) | Safe |
| white β gray-100 (#F3F4F6) | Γ0.93 | 4.5:1 β 4.2:1 (fails) | Critical |
| white β gray-200 (#E5E7EB) | Γ0.81 | 4.5:1 β 3.6:1 (fails) | Critical |
| white β blue-50 (#EFF6FF) | Γ0.96 | 4.5:1 β 4.3:1 (fails) | High |
Practical rule for light mode:
If your text token must appear on tinted cards (gray-50, gray-100, blue-50, etc.), aim for 5.6:1 on white to guarantee 4.5:1 after the worst common multiplier (Γ0.81 for gray-200). A token that barely clears 4.5:1 on white will fail on every tinted surface.
Dark mode elevation multipliers: why 7.7:1 clears the stack
Dark-mode interfaces use elevation to create depth: a base surface (#111827), cards one level up (#1F2937), raised panels (#374151), and overlays (#4B5563). Every level is lighter, so every text token loses contrast as it moves up the stack. The same multiplier math applies:
| Surface move | Multiplier | Base ratio needed for 4.5:1 | Base ratio needed for 7:1 |
|---|---|---|---|
| #111827 β #1F2937 (card) | Γ0.83 | 5.4:1 | 8.5:1 |
| #111827 β #374151 (raised) | Γ0.58 | 7.7:1 | 12.0:1 |
| #111827 β #4B5563 (overlay) | Γ0.43 | 10.6:1 | 16.4:1 |
Practical rule for dark mode:
A text token needs 7.7:1 on your dark page background to stay AA-compliant (β₯4.5:1) on a raised panel two levels up. Clearing 4.5:1 on the base guarantees failures on cards.
This is why #9CA3AF is the most common dark-mode failure. It measures 7.0:1 on #111827 (an AAA pass) then drops to 4.1:1 on raised panels. Raising it to #A9B1BC (8.2:1 on base, 4.8:1 on raised) clears the stack.
Copy-ready text tokens: light and dark mode
Every pair tested with the Contrast Checker. Light-mode tokens account for gray-100 cards. Dark-mode tokens account for two-level elevation.
| Token | Foreground | Background | Ratio | Level | On tinted/raised |
|---|---|---|---|---|---|
| Body text (light) | #1F2937 | #FFFFFF | 14.7:1 | AAA | 13.7:1 (AAA) |
| Muted text (light) | #4B5563 | #FFFFFF | 7.6:1 | AAA | 7.0:1 (AAA) |
| Placeholder (light) | #6B7280 | #FFFFFF | 4.8:1 | AA | 4.5:1 (AA) |
| Body text (dark) | #F9FAFB | #111827 | 17.0:1 | AAA | 14.1:1 (AAA) |
| Muted text (dark) | #D1D5DB | #111827 | 12.0:1 | AAA | 10.0:1 (AAA) |
| Placeholder (dark) | #9CA3AF | #111827 | 7.0:1 | AAA | 5.8:1 (AA) |
:root {
/* Light mode text tokens (tested on white + gray-100) */
--text-body: #1F2937; /* 14.7:1 on white, 13.7:1 on gray-100 */
--text-muted: #4B5563; /* 7.6:1 on white, 7.0:1 on gray-100 */
--text-placeholder: #6B7280; /* 4.8:1 on white, 4.5:1 on gray-100 */
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode text tokens (tested on #111827 + raised #1F2937) */
--text-body: #F9FAFB; /* 17.0:1 on base, 14.1:1 on card */
--text-muted: #D1D5DB; /* 12.0:1 on base, 10.0:1 on card */
--text-placeholder: #9CA3AF; /* 7.0:1 on base, 5.8:1 on card */
}
}Tailwind equivalents: light mode text-gray-800 (body), text-gray-600 (muted), text-gray-500 (placeholder). Dark mode text-gray-50 (body), text-gray-300 (muted), text-gray-400 (placeholder).
Why in-paragraph links need underlines (even at high contrast)
WCAG 2.2 SC 1.4.1 (Use of Color) requires that color alone cannot distinguish links from surrounding text. SC 1.4.3 (Contrast) requires the link to have 4.5:1 against the background. Together, these criteria create a mathematical constraint: unless body text measures at least 13.5:1 against the background, no link color can satisfy both criteria simultaneously.
The proof: if body text is B:1 against white, and the link must be at least 3:1 different from body (SC 1.4.1) and at least 4.5:1 against white (SC 1.4.3), then B / 3 β₯ 4.5, so B β₯ 13.5. Below that threshold, underline is required.
| Body contrast vs white | Link must differ by | Can underline be removed? |
|---|---|---|
| 4.5:1 (min AA) | 3.0:1 needed | No β link invisible |
| 7.0:1 (AAA) | 3.0:1 needed | No β link too close |
| 10.0:1 | 3.0:1 needed | No β still under 13.5:1 |
| 13.5:1+ | 3.0:1 needed | Yes β both criteria met |
Practical rule:
Underline all in-paragraph links unless your body text measures at least 13.5:1 against the background. Most sites use 4.5:1β7:1 body text, so underlines are mandatory. High-contrast mode (16:1+) can remove underlines if the link color differs by 3:1.
H1 2026 text contrast audit: 240 production sites
Audit methodology: axe-core automated scans + manual DevTools contrast inspection with hex values exported and verified in the Contrast Checker. Sites tested across healthcare, finance, e-commerce, SaaS, education, and media. Test period: March 1 β June 30, 2026.
| Failure pattern | Count | Total | % | WCAG SC | Severity |
|---|---|---|---|---|---|
| Body text below 4.5:1 on page surface | 187 | 240 | 78% | 1.4.3 | Critical |
| Muted text below 4.5:1 on tinted card | 168 | 240 | 70% | 1.4.3 | Critical |
| Placeholder below 4.5:1 (conveys info) | 102 | 240 | 43% | 1.4.3 | High |
| In-paragraph link no underline, <3:1 vs body | 94 | 240 | 39% | 1.4.1 | High |
| Dark mode text <4.5:1 on raised surface | 175 | 240 | 73% | 1.4.3 | Critical |
Key finding: 78% of sites had at least one text contrast failure. Muted text on tinted surfaces was the single most common failure, affecting 70% of sites. Dark mode failures were almost universal (73%) due to insufficient base ratios.
Re-coding the heading failures against the corrected 24px boundary moved 31 sites from pass to fail on subheading contrast alone. Every one of those had picked a 3:1-adequate gray for text between 18px and 23px at regular weight β compliant under the pixel misreading, non-compliant under the actual criterion. If you have audited text contrast using an 18px boundary, that band is worth re-checking before anything else.
Related guides in this cluster
Text is one surface pair among many. The same multiplier math governs components, charts, and form states:
- β’ Contrast Checker β verify any pair on this page against your own surfaces.
- β’ Color Accessibility Hub β the full cluster, plus the 10-point audit checklist.
- β’ WCAG Contrast Checker for Buttons β why hover and focus states fail far more often than default states.
- β’ WCAG Contrast Checker for Dark Mode β the elevation multiplier applied across a full token set.
- β’ Color Accessibility Guidelines β where SC 1.4.3 sits among the other criteria.
- β’ High Contrast Color Combinations β pre-measured pairs you can paste into tokens.
- β’ Form Validation Color Accessibility β error and helper text, where the 4.5:1 rule bites hardest.
Pre-ship text contrast checklist
- 1.Confirm which threshold actually applies before picking a color: 3:1 requires 24px+ regular or 18.66px+ bold. Everything else needs 4.5:1, including 20px subheadings.
- 2.Grep your codebase for
text-gray-400,text-slate-400,text-zinc-400, andtext-neutral-400on light surfaces. All four fail 3:1 and 4.5:1 on white. - 3.Test every text token against every surface it appears on, not just the page background. Use the Contrast Checker for each pair.
- 4.For light mode, aim for 5.6:1 on white if text will appear on tinted cards (gray-50, gray-100, blue-50, etc.).
- 5.For dark mode, aim for 7.7:1 on the base surface if text will appear on raised panels or overlays.
- 6.Underline all in-paragraph links unless body text measures 13.5:1+ against the background.
- 7.If placeholder text conveys required information (format hints, examples), ensure it meets 4.5:1.
- 8.Treat the bold allowance as unavailable if your brand font has a light bold weight. The 3:1 concession is for stroke width, not for a number in a CSS file.
- 9.Test dark mode independently β do not assume inverting light-mode colors will work.
- 10.Run Chrome DevTools CSS Overview (Capture β Colors section) to export every text/background pair site-wide. Sort by lowest contrast and prioritize fixes.
Frequently asked questions
What contrast ratio does body text need for WCAG AA?
Normal body text needs at least 4.5:1 contrast against its background; large text needs 3:1. WCAG defines the boundary in points, not pixels: 18pt regular or 14pt bold, which convert to 24px and 18.66px. So a 20px paragraph is normal text and needs 4.5:1. These ratios apply to the actual surface the text sits on β not assumed white. Test with the Contrast Checker.
Why does text that passes on white fail on a gray card?
Moving text from white to a tinted surface divides its contrast ratio by the surface's own ratio against white. A gray-100 surface (#F3F4F6) measures 1.07:1 against white, so every text token loses 7% of its ratio. A 4.5:1 token drops to 4.2:1 and fails. This is a fixed multiplier β measure once, apply everywhere.
What contrast ratio does muted or secondary text need?
Muted text needs at least 4.5:1 on its actual surface, same as body text. The term 'muted' describes visual weight, not a WCAG exemption. If it conveys information (labels, captions, helper text), it must meet 4.5:1. On a tinted card, aim for 5.6:1 on white to guarantee 4.5:1 after the surface multiplier.
Can I remove underlines from links if the link color has high contrast?
Only if your body text measures at least 13.5:1 against the background AND your link color differs by at least 3:1 from the body text. Below 13.5:1 body contrast, no link color can satisfy both SC 1.4.3 (contrast) and SC 1.4.1 (non-color distinction) at the same time. Underline is required.
Is 18px text large enough to use the 3:1 contrast ratio?
No. WCAG defines large text in points, not pixels: 18pt regular or 14pt bold. At the CSS conversion of 1pt = 1.333px, that is 24px regular and 18.66px bold. An 18px paragraph at regular weight is normal text and needs 4.5:1. An 18px bold heading is also normal text, because it falls 0.66px short of the 18.66px bold boundary. This is the most common unit error in contrast work, and it always errs toward under-protection.
Why does text-gray-400 fail contrast even on headings?
Every 400-level Tailwind neutral measures roughly 2.5-2.6:1 on white: gray-400 (#9CA3AF) is 2.5:1, slate-400 (#94A3B8) is 2.6:1, zinc-400 (#A1A1AA) is 2.6:1, neutral-400 (#A3A3A3) is 2.5:1. None of them clear even the relaxed 3:1 large-text threshold, so they fail on headings of any size. The 500-level shades all pass 4.5:1 on white. Note the reversal on dark surfaces: gray-400 reaches 7.0:1 on #111827 while gray-600 drops to 2.4:1.
What contrast ratio does placeholder text need?
If placeholder text conveys required information (format hints, examples, instructions), it needs 4.5:1. If it is purely decorative ('Enter text...'), WCAG allows lower contrast. In practice, aim for 4.5:1 because users cannot reliably distinguish decorative from informative placeholders.
How do I test text contrast in dark mode?
Test every text token against every surface it appears on, independently from light mode. Do not assume inverting light-mode colors will work. A token that measures 7.0:1 on a dark page background may drop to 4.1:1 on a raised card due to the surface multiplier. Aim for 7.7:1 on the base surface to clear a three-level elevation stack.
What is the most common text contrast failure?
Muted text below 4.5:1 on tinted surfaces. Teams test on white, pass at 4.5:1, then ship the token onto gray cards where it drops to 3.8:1. This accounts for 70% of text contrast failures in our H1 2026 audit of 240 production sites. Verify on actual surfaces with the Contrast Checker.