What this guide covers

  • Define roles before choosing colors.
  • Give every text pair a measured contrast target.
  • Use more than hue to communicate status.
  • Build dark mode as a separate contract.
  • Test real components, not just swatches.

Live checker

Check any text and background pair

WCAG thresholds: AA 4.5:1 normal and 3:1 large; AAA 7:1 normal and 4.5:1 large.

WCAG 2.2

Large heading at 24px bold

Normal body copy at 16px. This is the size most people read, and the size most palettes fail.

Small label at 14px
Contrast ratio7.39:1
AA normalPass
AA largePass
AAA normalPass
AAA largePass

Start with roles, not colors

An accessible palette begins with a list of jobs. What needs a background? What needs a surface? What needs body text, muted text, a border, a primary action, a focus ring, a success state, a warning and an error?

Once the jobs are clear, each color has a constraint. The body text color must reach 4.5:1 on the page background. The primary button text must reach 4.5:1 on the primary color. The focus ring must be visible on every surface it can appear on.

This is much easier than choosing a beautiful palette and then trying to make it accessible after the fact. The roles tell you which pairs matter.

Give every text pair a contrast budget

Create a table of the pairs the product uses and the minimum ratio for each. Body text on background: 4.5:1. Muted text on background: 4.5:1. Button text on primary: 4.5:1. Large headings: 3:1. Borders and focus rings: 3:1.

Measure the pairs with a contrast checker and record the result. When a token changes, re-measure the pairs that depend on it. A single background change can affect five text pairs.

If a pair fails, do not average it with another pair or hope the font weight saves it. Fix the pair or choose a different role for the color.

PairMinimumWhy
Body text / background4.5:1Normal-size reading
Muted text / background4.5:1Still content
Button text / primary4.5:1Action label
Large heading / background3:1Large text threshold
Border / surface3:1Meaningful boundary
Focus ring / surface3:1Keyboard visibility

Status colors need more than hue

Success, warning and danger are semantic. They need to be distinguishable from each other, from the primary action and from the background. But hue alone is not enough, especially for people with color-vision deficiency.

Pair each status color with an icon, a label or a pattern. A red border plus the word 'Error' is clearer than a red border alone. A green check plus 'Saved' is clearer than a green dot.

Check the status colors in both light and dark mode. A deep red that works on white may disappear on a dark surface. Adjust the lightness separately from the brand accents.

Dark mode is a separate contract

A dark theme needs its own background, surface, elevated surface, border, text and muted text. The same brand color may need a different lightness to remain visible.

Do not use pure black and pure white as the default pair. A very dark neutral and an off-white are often easier to read and give the surface hierarchy more room.

Re-measure every pair in dark mode. A palette that passes in light mode can fail in dark mode, especially for muted text and borders.

Test real components

Swatches do not have interactive states. Components do. A button has rest, hover, active, focus and disabled states. An input has a border, a placeholder, a value, a helper text and an error state. A card has a surface, a border and sometimes a shadow.

Test each component in both themes. Check the worst-case pair, not the most common one. A disabled button may be exempt from some contrast requirements, but it still needs to be distinguishable from an enabled button.

Use a real screen with real content. A pricing card, a form and a dashboard will expose different problems. The palette is ready when all three are readable and coherent.

Testing beyond contrast

Keyboard navigation, focus order, screen-reader labels, touch target size, motion preferences and zoom all affect accessibility. A palette cannot fix a missing label or an unreachable button.

Test with a keyboard, a screen reader and a zoomed viewport. Check that focus is visible and that content does not disappear when the page is zoomed to 200%.

If you can, test with people who have different color vision and different devices. Their experience will reveal issues that a contrast checker cannot.

  • Tab through the page and confirm focus is visible.
  • Check the page at 200% zoom.
  • Confirm every icon-only button has a label.
  • Check reduced-motion behavior.
  • Test status messages with a screen reader.

Keep the palette accessible over time

Accessibility regressions usually happen after the initial launch. A new component introduces a new color pair. A brand refresh changes the primary without re-checking the button text. A dark-mode tweak reduces the border contrast.

Add the contrast table to the design-system documentation and make it part of the review process. When a token changes, the table changes with it.

A palette is not accessible because it passed once. It is accessible because the team keeps checking the pairs that matter.

That discipline is especially important when a new component reuses an existing token in a new context, because the pair may never have been measured in that combination before.

PhotoColors Team

We build browser-based color tools and write about the unglamorous parts of color systems: contrast, tokens, handoff and the difference between a pretty palette and a usable one.

About PhotoColors