Contrast is a property of a pair: a foreground and the background directly behind it. A palette cannot pass or fail on its own. What matters is each pair that meets in the rendered page, including muted labels, placeholder text, input borders, focus rings, and text over gradients or translucent panels.

The thresholds

  • Normal text: at least 4.5:1 for Level AA (success criterion 1.4.3), 7:1 for AAA.
  • Large text: at least 3:1 for AA, 4.5:1 for AAA. Large means at least 18pt or 14pt bold, which W3C gives as roughly 24px and 18.5px.
  • Non-text contrast (1.4.11, AA): 3:1 for the visual information needed to identify a control and its state, such as an input's border, a checkbox outline, or a meaningful icon, and for parts of graphics needed to understand them.

Ratios are not rounded. W3C's own example: 4.499:1 does not meet 4.5:1. If a checker shows 4.5 but the underlying value is 4.47, the pair fails.

Some text has no requirement: logos and brand names, text in inactive (disabled) controls, purely decorative text, and incidental text inside a photo. Brand guidelines do not exempt anything else. A pale brand gray used for body copy still has to pass.

How the ratio is calculated

Each color is converted to relative luminance, a value from 0 (black) to 1 (white) that weights green most and blue least, following how bright each channel appears. The ratio is (lighter + 0.05) / (darker + 0.05), so it runs from 1:1 to 21:1. This is the calculation every WCAG 2 checker performs:

javascript
function luminance(hex) {
  const [r, g, b] = [0, 2, 4].map((i) => {
    const c = parseInt(hex.replace("#", "").slice(i, i + 2), 16) / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrast(a, b) {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
  return (hi + 0.05) / (lo + 0.05);
}

contrast("#64748b", "#ffffff"); // 4.76 — passes for normal text
contrast("#94a3b8", "#ffffff"); // 2.56 — fails even for large text

Those two grays look close in a swatch row, but only one of them can carry body text on white. The ratio is not linear in how different two colors look, which is why judging contrast by eye is unreliable, especially for mid-tones and saturated colors.

Text: check the muted and the small

Headings rarely fail. The failures are in secondary text: captions, timestamps, helper text under inputs, table metadata, footer links, and placeholders. Placeholder text is text, so if it carries information it needs 4.5:1 like any other label. Better still, do not rely on a placeholder for the label at all, since it disappears as soon as someone types.

  • List every text style in the design system and the surfaces it can appear on. A caption color that passes on white may fail on a gray card.
  • Check the hover and pressed colors of text links, not only the default.
  • Treat large-text exceptions carefully. A 20px regular heading is not large text under WCAG; a 24px regular one is.

Controls and the 3:1 rule

Non-text contrast applies to whatever someone needs to see to find and operate a control. For a text input, that is usually the border. A teal border of #14b8a6 on a pale teal page (#f0fdfa) measures 2.39:1 and fails, even though it looks clearly colored. A darker teal such as #0f766e measures 5.25:1 on the same background.

A button with a visible text label is different. W3C's guidance says the button's boundary does not need 3:1 against the page when the text itself identifies it. The label text inside the button still needs 4.5:1 against the button's fill. White text on #3b82f6 measures 3.68:1, which only passes for large text; on #2563eb it measures 5.17:1.

Focus indicators

A focus indicator has to be visible (2.4.7, AA) and, under non-text contrast, has to contrast with the colors next to it. The stricter AAA criterion, Focus Appearance (2.4.13), asks for an indicator at least as large as a 2 CSS pixel perimeter around the control, with at least 3:1 contrast between its focused and unfocused states.

A single-color ring fails on some surfaces: a blue ring disappears against a blue button. A two-tone ring, a light inner ring plus a dark outer ring, stays visible on light, dark, and colored backgrounds:

css
:focus-visible {
  outline: 2px solid #0f172a;          /* dark outer ring */
  outline-offset: 2px;
  box-shadow: 0 0 0 2px #ffffff;       /* light ring in the offset gap */
}

In Tailwind v4: focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900 focus-visible:shadow-[0_0_0_2px_#fff]. Check it in Windows High Contrast (forced colors) mode as well, where box-shadow is removed and the outline is what remains.

Do not rely on color alone

Use of Color (1.4.1, A) is a separate requirement from contrast. A red border on an invalid field, or links in a paragraph that differ from body text only by hue, communicate through color alone. Add a second cue: an error message and icon next to the field, an underline on links inside running text. A green and a red status dot with identical shape will look the same to many people with color vision deficiencies.

Gradients, images, and glass

When the background changes behind the text, the pair is no longer one pair. Measure the text against the lightest and darkest colors it actually overlaps. For a gradient, that is usually the two ends. For a photo, it is the brightest area under the text, and W3C lists background images without sufficient contrast as a known failure (F83).

For a translucent panel, calculate the color the panel actually produces. A fill of color F at opacity a over background B becomes roughly a × F + (1 − a) × B per channel. 60% white over #7c3aed comes out near #cbb0f8; on that, #475569 text measures about 4.0:1 (fails) and #334155 about 5.5:1 (passes). Blur averages the backdrop, which usually helps, but the safe measurement is against the unblurred extreme.

  • If the background is a photo you do not control, such as user uploads, put the text on an opaque or nearly opaque surface.
  • Re-measure after changing fill opacity, blur, or the backdrop image. Each one changes the effective background.
  • A dark overlay between a photo and white text is easier to verify than relying on the photo staying dark. Size it for the worst case, a pure white area: black at 50% over white gives #808080, where white text measures only 3.95:1; at 60% it gives #666666 and 5.74:1.

Dark mode is a second set of pairs

A dark theme is not the light theme inverted for contrast purposes. On a #0f172a background, #94a3b8 measures 6.96:1 and works for body text, while #64748b drops to 3.75:1. On white it is the other way round: #64748b passes and #94a3b8 fails. Reusing the same muted token in both themes guarantees that one of them fails. Saturated accents also behave differently: a mid blue that passes as a button fill with white text on a light page may look too heavy on dark, and the lighter replacement may then fail with white text.

A practical checking workflow

  1. Write down the pairs, not the colors: text on surface, muted text on surface, text on primary button, input border on surface, focus ring on surface, for each theme.
  2. Measure each pair and record the ratio next to it, so a later palette change can be reviewed against the list.
  3. Fix by changing one color at a time, usually darkening the foreground on light themes or lightening it on dark themes, then measure again.
  4. Check the rendered page, including hover, focus, error, and disabled states, at the real font sizes.

The palette visualizer places five color roles (text, background, primary, secondary, accent) on a sample website and dashboard and switches between light and dark themes, which is a quick way to see pairs in context before measuring them. Browser developer tools also show a contrast ratio in the color picker for a selected text element.

References

W3C: Understanding SC 1.4.3 Contrast (Minimum)

W3C: Understanding SC 1.4.11 Non-text Contrast

W3C: Understanding SC 2.4.13 Focus Appearance

W3C: Understanding SC 1.4.1 Use of Color

W3C: WCAG 2.2 (relative luminance and contrast ratio definitions)