// guide

Color and Contrast: Beyond the Ratio

Contrast ratios are the starting point, not the finish line. This guide covers who benefits from good contrast, what WCAG actually requires, how to check your work, and practical CSS techniques for building color systems that hold up across themes and user preferences.

beginner visual-design

// 01 · why contrast matters

Why Contrast Matters

When developers hear "contrast accessibility," they often think of screen reader users. But contrast primarily affects people who can see — they just can't see well enough when contrast is poor.

The numbers are significant. Approximately 300 million people worldwide have some form of color vision deficiency (commonly called "color blindness"), and around 250 million live with low vision that isn't fully correctable with glasses or contacts. These are permanent conditions, but they're only part of the picture.

Situational impairments affect everyone at some point: reading a phone screen in bright sunlight, working on a laptop in a dim room, or dealing with screen glare on a train. Age is a factor too — the lens of the human eye yellows over time, reducing contrast sensitivity. Most people over 40 experience some degree of this.

Good contrast isn't an accommodation for a small group. It's a baseline that makes content more readable for hundreds of millions of people across a wide range of circumstances.

// 02 · the contrast ratios

The Contrast Ratios

WCAG defines contrast as a ratio between the relative luminance of two colors, ranging from 1:1 (no contrast — identical colors) to 21:1 (maximum contrast — black on white). The required ratio depends on what you're styling and the conformance level you're targeting.

Requirement Ratio WCAG Criterion Level
Normal text 4.5:1 SC 1.4.3 AA
Large text 3:1 SC 1.4.3 AA
Enhanced (all text) 7:1 / 4.5:1 large SC 1.4.6 AAA
Non-text elements 3:1 SC 1.4.11 AA
What counts as "large text"? Large text is defined as 18pt (24px) or larger at regular weight, or 14pt (18.66px) or larger at bold weight. Large text gets a lower contrast requirement because its size already makes it more legible. If you're unsure, use the stricter 4.5:1 ratio — it satisfies both normal and large text requirements.

Note that the non-text contrast requirement (3:1) applies to UI component boundaries, focus indicators, icons that convey meaning, and parts of graphical objects needed to understand the content. Decorative graphics and logos are exempt.

// 03 · checking contrast

Checking Contrast

You don't need to calculate luminance ratios by hand. Several tools can check contrast for you at different stages of your workflow.

  • Browser DevTools Chrome's color picker shows the contrast ratio directly when you inspect a text element — look for the contrast section in the color popover. Firefox's accessibility inspector also flags contrast issues and lets you simulate color vision deficiencies.
  • Online calculators WebAIM Contrast Checker lets you enter two hex values and instantly see whether they pass AA or AAA for normal and large text. Useful during design review.
  • Desktop applications Colour Contrast Analyser by TPGi includes an eyedropper for picking colors from any application on screen — not just the browser. Helpful for checking contrast in design tools, PDFs, or native apps.
  • Automated audits axe-core and Lighthouse's accessibility audit can catch many contrast failures in CI pipelines or during development. They test rendered colors against WCAG thresholds automatically.
Automated tools only go so far Automated contrast checkers catch roughly 30% of real-world contrast issues. They work well for solid text on solid backgrounds, but they can't evaluate text over images, gradients, semi-transparent overlays, or dynamically changing content. Manual checking is still essential — especially for hero sections, banners, and any text layered on photography.

// 04 · non-text contrast (sc 1.4.11)

Non-Text Contrast (SC 1.4.11)

SC 1.4.11 is often overlooked because developers focus on text. But UI components and graphical objects also need at least 3:1 contrast against adjacent colors. "Adjacent" means the color immediately next to the element — usually the background, but sometimes a neighboring element.

Here's what falls under non-text contrast:

  • Form field borders — the border (or other visual boundary) of text inputs, selects, and textareas must have 3:1 contrast against the surrounding background. A light gray border on a white background often fails. See the Forms pattern for accessible field styling.
  • Focus indicators — the focus outline or ring must contrast at 3:1 against the background. This site uses a 2px solid outline with a color that meets this threshold in both light and dark themes.
  • Icon-only buttons — if a button uses only an icon (no visible text label), the icon itself must have 3:1 contrast. The icon is the only visual affordance telling the user what the button does.
  • Custom checkboxes and radio buttons — when you restyle native form controls, the custom visual must maintain 3:1 contrast for the boundary of the control and any state indicator (the check mark, the filled dot).
  • Chart and graph elements — bars, lines, pie segments, and data points that need to be distinguishable must contrast against each other or against the chart background.
Examples from this site The Switch toggle track maintains 3:1 contrast against the page background in both states. The Toast dismiss button icon contrasts against the toast background. Form field borders in the Forms pattern use a border color that passes 3:1 against the page surface.

// 05 · color as the only indicator (sc 1.4.1)

Color as the Only Indicator (SC 1.4.1)

SC 1.4.1 (Use of Color) is one of the most frequently violated criteria. It says: color must not be the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. You can still use color — you just can't use it alone.

Failure: Links distinguished only by color Body text links that are the same font weight, size, and style as surrounding text, with only a color change to indicate they're clickable. Users with color vision deficiency may not notice them at all.
Fix: Add an underline Underline links within body text, or provide a non-color visual indicator (underline, bold weight, icon). The underline can be subtle — text-decoration-thickness and text-underline-offset let you style it tastefully while keeping it visible.
Failure: Form errors shown only in red Turning an input border red when validation fails, with no other indication. A user who can't distinguish red from other colors won't know which field has an error.
Fix: Add an icon and a text message Pair the red border with an error icon and a descriptive text message explaining what went wrong. See the Forms pattern for accessible error handling that combines color, icons, text, and ARIA attributes.
Failure: Chart data using only color A bar chart or line graph where the only way to tell the data series apart is by color. If two series look the same hue to a user with deuteranopia, the chart is unreadable.
Fix: Add patterns, labels, or shapes Use different fill patterns (stripes, dots, crosshatch), direct data labels, or distinct marker shapes (circles, squares, triangles) alongside color. This makes the data distinguishable through multiple visual channels.
Failure: Status indicators using only color A dashboard that shows "online" as a green dot and "offline" as a red dot, with no text or icon difference.
Fix: Add text labels or icons Include text labels ("Online," "Offline") or use distinct icons (a check mark for online, an X for offline) in addition to the color. The color reinforces the meaning; it doesn't carry it alone.

// 06 · dark mode considerations

Dark Mode Considerations

Adding dark mode to a site effectively doubles your contrast work. Colors that pass on a light background may fail on a dark one, and vice versa. A few things to watch for:

  • Light text on dark backgrounds needs re-checking. A medium blue that passes 4.5:1 against white (#fff) will likely fail against a dark gray (#1a1a2e). You need to test every text-background pair in both themes.
  • Translucent overlays compound the problem. If you use rgba() or opacity on overlays, the effective contrast depends on what's behind them. A semi-transparent dark overlay on a dark background produces very low contrast against light text. Test the computed colors, not just the overlay values.
  • Both themes must pass independently. It's not enough for light mode to pass and assume dark mode is fine. WCAG requirements apply to whatever the user sees.

This site uses CSS custom properties scoped to a [data-theme="dark"] selector, which makes it straightforward to define distinct values for each theme:

:root {
  --color-text: #1a1a2e;
  --color-bg: #ffffff;
  --color-primary: #2563eb;
}

[data-theme="dark"] {
  --color-text: #e2e8f0;
  --color-bg: #1a1a2e;
  --color-primary: #60a5fa;
}

Each value can be independently tuned to meet contrast ratios in its own context. The primary blue shifts from #2563eb in light mode to #60a5fa in dark mode — a lighter, more saturated variant that maintains sufficient contrast against the dark background.

Real-world example from this site The primary button hover state originally had insufficient contrast in dark mode. The fix was to adjust the hover background color specifically in the [data-theme="dark"] scope without affecting the light theme. This is exactly the kind of issue that only shows up when you test both themes — and exactly why dark mode contrast deserves dedicated attention.

// 07 · practical css techniques

Practical CSS Techniques

CSS provides several mechanisms for building color systems that adapt to user preferences and maintain contrast across contexts.

Custom properties for theme-safe contrast

Define all colors as custom properties at the root level, then override them for dark mode. Components reference the properties, not hard-coded values, so they automatically adapt when the theme changes.

/* All color decisions happen here */
:root {
  --color-text: #1a1a2e;
  --color-text-secondary: #4a5568;
  --color-border: #d1d5db;
  --color-surface: #f7f8fa;
}

[data-theme="dark"] {
  --color-text: #e2e8f0;
  --color-text-secondary: #a0aec0;
  --color-border: #374151;
  --color-surface: #232340;
}

/* Components never use raw color values */
.card {
  color: var(--color-text);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}

Respecting high contrast preference

The prefers-contrast: more media query lets you detect when a user has requested increased contrast through their operating system settings. Use it to bump up border visibility, text weight, or color intensity.

@media (prefers-contrast: more) {
  :root {
    --color-text-secondary: #1a1a2e;
    --color-border: #1a1a2e;
  }

  [data-theme="dark"] {
    --color-text-secondary: #f7f8fa;
    --color-border: #f7f8fa;
  }
}

Windows High Contrast (forced-colors)

Windows High Contrast Mode overrides your colors entirely. The forced-colors media query lets you detect this and adjust layout elements that depend on color — like borders that become invisible when the OS removes your custom colors.

@media (forced-colors: active) {
  .button {
    border: 2px solid ButtonText;
  }

  .card {
    border: 1px solid CanvasText;
  }

  .focus-ring {
    outline: 2px solid Highlight;
  }
}

In forced-colors mode, use the CSS system color keywords (Canvas, CanvasText, ButtonText, Highlight, etc.) instead of your own color values. The browser maps these to the user's chosen high-contrast palette.

// 08 · next steps

Next Steps

Contrast is one of the few accessibility requirements an automated tool can measure precisely — but only in the states it can reach. When you verify a design, check every state (default, hover, focus, disabled) against the targets above. The Accessibility Testing Checklist folds contrast into a structured visual-and-responsive pass, and developers can confirm their implementation against the Developer Accessibility Checklist.