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 sizeAA (Legal minimum)AAA (Enhanced)Examples
Normal text
<18pt regular (<24px) or <14pt bold (<18.66px)
4.5:17:1Body copy, labels, captions, helper text, form placeholders, and most subheadings
Large text
β‰₯18pt regular (β‰₯24px) or β‰₯14pt bold (β‰₯18.66px)
3:14.5:1Display 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:

ClassSizeRegular weight needsBold weight needsNote
text-xs12px4.5:14.5:1Well below both boundaries
text-sm14px4.5:14.5:114px is not 14pt β€” common unit slip
text-base16px4.5:14.5:1Body copy baseline
text-lg18px4.5:14.5:1Danger zone β€” 0.66px short of bold large
text-xl20px4.5:13:1Danger zone at regular weight
text-2xl24px3:13:1First size large at regular weight
text-3xl30px3:13:1Large
text-4xl36px3:13:1Large

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:

TokenHexOn whiteLarge text (3:1)Normal text (4.5:1)
gray-400#9CA3AF2.5:1FailFail
slate-400#94A3B82.6:1FailFail
zinc-400#A1A1AA2.6:1FailFail
neutral-400#A3A3A32.5:1FailFail
gray-500#6B72804.8:1PassPass
slate-500#64748B4.8:1PassPass
zinc-500#71717A4.8:1PassPass
neutral-500#7373734.7:1PassPass

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:

new_ratio = (L_text + 0.05) / (L_surface + 0.05)
= 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 moveMultiplierExample (4.5:1 token)Severity
white β†’ gray-50 (#F9FAFB)Γ—0.984.5:1 β†’ 4.4:1 (still passes)Safe
white β†’ gray-100 (#F3F4F6)Γ—0.934.5:1 β†’ 4.2:1 (fails)Critical
white β†’ gray-200 (#E5E7EB)Γ—0.814.5:1 β†’ 3.6:1 (fails)Critical
white β†’ blue-50 (#EFF6FF)Γ—0.964.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 moveMultiplierBase ratio needed for 4.5:1Base ratio needed for 7:1
#111827 β†’ #1F2937 (card)Γ—0.835.4:18.5:1
#111827 β†’ #374151 (raised)Γ—0.587.7:112.0:1
#111827 β†’ #4B5563 (overlay)Γ—0.4310.6:116.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.

TokenForegroundBackgroundRatioLevelOn tinted/raised
Body text (light)#1F2937#FFFFFF14.7:1AAA13.7:1 (AAA)
Muted text (light)#4B5563#FFFFFF7.6:1AAA7.0:1 (AAA)
Placeholder (light)#6B7280#FFFFFF4.8:1AA4.5:1 (AA)
Body text (dark)#F9FAFB#11182717.0:1AAA14.1:1 (AAA)
Muted text (dark)#D1D5DB#11182712.0:1AAA10.0:1 (AAA)
Placeholder (dark)#9CA3AF#1118277.0:1AAA5.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 whiteLink must differ byCan underline be removed?
4.5:1 (min AA)3.0:1 neededNo β€” link invisible
7.0:1 (AAA)3.0:1 neededNo β€” link too close
10.0:13.0:1 neededNo β€” still under 13.5:1
13.5:1+3.0:1 neededYes β€” 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 patternCountTotal%WCAG SCSeverity
Body text below 4.5:1 on page surface18724078%1.4.3Critical
Muted text below 4.5:1 on tinted card16824070%1.4.3Critical
Placeholder below 4.5:1 (conveys info)10224043%1.4.3High
In-paragraph link no underline, <3:1 vs body9424039%1.4.1High
Dark mode text <4.5:1 on raised surface17524073%1.4.3Critical

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:

Pre-ship text contrast checklist

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.