How to Build an Accessible Color Palette From Scratch

A practical order of operations: lock the text colors first, derive the accents, give every semantic color a 3:1 UI fallback, and test pairs as you go.

Most palettes are built in the wrong order: pick five colors that look great together, ship them, then discover in an audit that half the combinations are unreadable. Accessibility isn’t a paint job you apply at the end — it’s a set of constraints that are nearly free if you start with them. Here’s the order of operations that works.

1. Lock the text colors first

Before any brand color: choose your page background, your body text color, and your secondary (muted) text color. Body text should clear 7:1 (AAA — cheap here, worth it), and muted text must still clear 4.5:1. The most common palette failure on the web is a muted gray that dips to 3.8:1 because it looked “calmer.”

Body — 17.4:1Primary text
#1A1A1Atext#FFFFFFbackground
Muted — 5.3:1Secondary text, still AA
#6B6B6Btext#FFFFFFbackground

2. Derive the brand and accent colors

Now bring in the brand color — as a role, not a hex. Decide where it appears: filled buttons (brand background + white or black text), links and interactive text (brand-as-text on the page background), and decorative moments (no constraint). Each role gets its own variant, tuned in OKLCH so hue never drifts — same hue, adjusted lightness. For picking companions to the brand hue — an accent, a secondary — harmony schemes at constant perceptual lightness do the heavy lifting; that’s exactly what the palette generator produces, with a contrast score on every swatch.

3. Give every semantic color a working pair

Success green, warning amber, danger red: each needs to function two ways —

  • as a UI element (badge fill, border, icon): 3:1 against the surface it sits on;
  • as text (error messages, inline status): 4.5:1 against the page background.

Amber is the classic casualty — a yellow-orange that reads as “warning” is rarely 4.5:1 on white. The standard answer is two tokens per semantic color: a vivid fill (with dark text on it) and a darkened text variant.

4. Never let color carry meaning alone

Roughly 1 in 12 men has some form of color-vision deficiency; your red/green status dots are identical to them. The palette isn’t finished until every color-coded state also differs by shape, icon, label, or position. This costs nothing at design time and is expensive to retrofit.

5. Test pairs, not swatches

A palette isn’t five colors — it’s every combination you’ll actually render. Enumerate them (text on background, text on brand, badge on card, icon on muted surface…) and check each one. It’s minutes of work with a contrast checker — or one call to a batch contrast API if your palette lives in code. Save the passing pairs somewhere findable; on PaletteSense the heart icon keeps them one click away.

The checklist

  1. Background + body text (7:1) + muted text (4.5:1) locked first
  2. Brand color split into roles: fill, text, decoration
  3. Semantic colors: 3:1 as UI, 4.5:1 as text — two tokens if needed
  4. No meaning carried by color alone
  5. Every real pair tested, not just the swatches

Follow that order and the audit becomes a formality — the palette was born compliant.

Try it in the tool

Start from any base color: generate a scored palette, browse 31,000+ named colors with their contrast data, or check any pair in the contrast checker.