How to Build an Accessible Color Palette That Still Looks Good
WCAG contrast ratios depend on lightness, not hue. Learn the math, the AA and AAA thresholds, the “color is not the only signal” rule, and a workflow for testing every pairing before you commit to a palette.
WCAG defines three contrast thresholds: 4.5:1 for normal text (AA), 3:1 for large text and UI components (AA), and 7:1 for normal text (AAA). The contrast ratio depends almost entirely on the lightness difference between two colors, not on the hues — a vivid teal on a dark surface can pass AAA, a muted sage on a slightly lighter sage will fail AA. Build the palette, test every text-on-surface pairing with a contrast checker, and never rely on color alone to convey meaning.
The single most common misconception about accessible color palettes is that they have to be dull. They do not. WCAG contrast ratios depend almost entirely on the lightness difference between two colors, not on the hues. A vivid teal on a sufficiently dark surface can pass AAA. A muted sage on a slightly lighter sage will fail AA. Once you internalize that contrast is a math problem, palette building becomes a solvable exercise rather than a creative compromise.
This guide covers the math, the thresholds, the "color is not the only signal" rule, and a workflow for building and testing a palette that passes accessibility without looking sterile.
The numbers, in one table
WCAG 2.1 defines three contrast thresholds, all measured as the relative luminance ratio between a foreground and a background color:
- 4.5:1 — the minimum for normal-size text at Level AA. This is the threshold most organizations target, and the one referenced by the ADA, the European Accessibility Act, and most national laws.
- 3:1 — the minimum for large text (18pt regular or 14pt bold) at Level AA. Also the minimum for non-text UI components like button borders, input outlines, and icons.
- 7:1 — the minimum for normal-size text at Level AAA. Aspirational rather than mandatory; reserved for critical content where readability is non-negotiable.
The scale runs from 1:1 (identical colors, no contrast) to 21:1 (pure black on pure white, maximum contrast). Anything below 4.5:1 is functionally unreadable for many users, especially in low light or on low-quality displays.
How the contrast ratio is calculated
The math is straightforward once you have the inputs. WCAG defines the relative luminance of a color as:
For each RGB channel (0–1, not 0–255):
if channel ≤ 0.03928:
linear = channel / 12.92
else:
linear = ((channel + 0.055) / 1.055) ** 2.4
L = 0.2126 * R_linear + 0.7152 * G_linear + 0.0722 * B_linear
Contrast = (L_lighter + 0.05) / (L_darker + 0.05)
Two important properties of this formula:
- The gamma correction (the 2.4 power) is the whole game. Human vision is much more sensitive to small differences in dark colors than in light ones. The gamma curve accounts for that by compressing the dark end and expanding the light end. It is why a "black" with luminance 0.0 and a "dark gray" with luminance 0.1 look very different, while a "light gray" with luminance 0.9 and white with luminance 1.0 look almost the same.
- The green channel matters most. The 0.7152 weight on green means a green-dominant color contributes much more to the perceived luminance than a red- or blue-dominant one. A pure green of
#00ff00has higher relative luminance than a pure blue of#0000ff, even though both are at the maximum saturation.
You do not need to do this math by hand for every pairing. A contrast checker that runs in your browser handles it for any two colors in any format (HEX, RGB, HSL), gives you the ratio, and tells you which WCAG level each pairing passes.
Where most palettes fail
The most common failure is not picking a "wrong" color — it is choosing a light color for body text on a light background, or a dark color on a dark one. The brand color is often a saturated mid-tone that looks great in a logo but is unreadable for body text. Two specific patterns to watch for:
- Brand color as link text on a white background. Most brand blues hover around 4:1 contrast at best. They pass for large headings and UI elements (where 3:1 is enough) but fail for body links. The fix: define a separate "interactive text" variant of the brand color that is darkened to pass 4.5:1, and reserve the bright brand color for non-text contexts (buttons, accents, decorative elements).
- Light gray for "secondary" text. The convention of using a very light gray for secondary or disabled text makes it unreadable for everyone, not just users with low vision. Even "disabled" text should hit 3:1 against its background. The fix: use a darker gray, around 4.5:1, for secondary text, and reserve the very light grays for non-text decoration.
Building the palette
A practical workflow that produces an accessible palette without endless tweaking:
1. Start with the brand color
Pick the one color that has to be in the palette no matter what — usually a saturated mid-tone that anchors the identity. Use a color picker to lock it down. Then test its contrast against white and against your planned background.
If the brand color does not pass 4.5:1 against white for text use, define a darker variant of it for text applications. Keep the original brand color for large decorative elements, buttons (where 3:1 is enough for the button background), and accents. Most palettes end up with two variants of the brand color: the original (for decoration) and a darker "text-safe" version.
2. Build the neutrals
Most of your text will be neutral, not colored. Define a scale of grays from near-black (body text, AAA) to near-white (background). At a minimum:
- Body text — near-black, 7:1+ against the background (AAA).
- Secondary text — medium gray, 4.5:1+ (AA for normal text).
- Disabled or placeholder text — at least 3:1 (AA for large text and UI).
Skipping straight from 7:1 to 3:1 in one jump (the "secondary vs disabled" gap) is the most common accessibility regression. Pick intermediate values, not just two.
3. Add the semantic colors
These are the colors that mean something: green for success, red for errors, yellow/amber for warnings, blue for informational messages. Each needs to meet contrast requirements when used for text or icon elements.
The hardest pair is red and green. They look distinct to most users, but red-green color blindness (the most common form) makes them hard to tell apart. The fix is to use a blue-toned red and a teal-toned green — they are distinguishable for color-blind users, and they are also more "modern" looking than pure red and pure green.
4. Define the accent colors
These are the fun ones — the bright colors used for hover states, highlights, gradients, and illustrations. They do not always carry informational meaning, so contrast requirements are less strict. But test them against the backgrounds they will sit on anyway; nothing kills a design like an accent color that disappears on a colored surface.
5. Test every pairing
This is the step most teams skip. Build a contrast matrix: a grid where every foreground color in the palette is checked against every background color. The cells that pass AA get a checkmark; the cells that fail get flagged. The matrix becomes a reference document for the team, so designers and developers know which combinations are safe to use and which are off-limits.
A few of the pairings worth testing in the matrix:
- Body text on the page background
- Body text on each card / surface color
- Link text on the page background
- Link text on each card / surface color
- Button text on the button background (for every button variant)
- Icon color on the page background
- Focus ring on the page background and on cards
- Error / success / warning text on the page background
This list is shorter than the actual matrix; treat it as a starting point. A small palette (3 brand colors + 5 grays + 4 semantic colors) gives you 12 × 12 = 144 pairings. Most will pass trivially; the ones that don't are the design decisions that need attention.
The "color is not the only signal" rule
WCAG Success Criterion 1.4.1 is the one most teams forget: color is not the only visual means of conveying information. This applies to:
- Form validation. Required fields should not be marked only with red. Add an asterisk and a label that says "Required".
- Error states. Do not just turn the border red — add an error icon and a text description of the problem.
- Charts. Do not rely on a single color to distinguish data series. Use distinct patterns, line styles, or shapes alongside color.
- Status indicators. A green dot for "online" is not enough — add a label that says "Online" or an icon that conveys the state without color.
A useful test: convert the design to grayscale. If you can still understand every element, your use of color is on the right track. If something becomes ambiguous, you are relying on color alone and you have an accessibility bug.
Real-device testing catches what numbers miss
Automated contrast checkers catch mathematical contrast failures. They do not catch perceptual issues. A pairing that passes 4.5:1 on a calibrated monitor in a dark office might fail in bright sunlight on a phone with a cheap LCD. A few habits that catch the rest:
- Test on a budget Android phone in bright light. If you cannot read the text, neither can many of your users.
- Test on an older laptop with a dim screen. The display is closer to what a user with low vision might see.
- Run your designs through a color blindness simulator. The three main types — protanopia (red weakness), deuteranopia (green weakness), tritanopia (blue weakness, rare) — account for about 8% of men and 0.5% of women.
The simulator catches hues that are too similar. The real-device test catches combinations that mathematically pass but perceptually fail. Both are worth doing.
Putting it all together
A practical, accessibility-first palette build is a 5-step process that takes an afternoon:
- Lock the brand color in a color picker.
- Generate a neutral gray scale (5–7 steps) with a palette generator.
- Add 4 semantic colors: success, error, warning, info.
- Test every intended text-on-surface pairing with a contrast checker. Save the resulting matrix as a reference document.
- Convert one of your designs to grayscale and check that everything is still readable.
The result is a palette that passes AA everywhere it matters, AAA for body text, and looks intentional rather than default. The math is the easy part; the discipline of testing every pairing is what makes the difference between a palette that is technically accessible and one that is genuinely usable for everyone.