Huely color system builder

Huely/Guides/Accessible color palettes

How to build an accessible color palette

Most palettes fail accessibility for the same reason: they were chosen as five swatches side by side, and an interface never shows them that way. Building in the opposite order — from the pairings out — costs nothing and removes almost all of the repair work.

Published · Last reviewed

Why palettes fail

A palette generator shows you five colors in a row, all the same size, none of them on top of another. An interface shows you text on a background, a label on a card, a button fill under white type. Every one of those is a pair, and a pair is the only thing contrast has an opinion about. A palette can look balanced as a strip and contain no usable text pair at all.

The second failure mode is subtler: the palette is fine and the interface still is not, because the interface needs twenty colors and the palette has five. Border, hover, pressed, disabled, muted caption, subtle divider — each gets improvised at implementation time, usually by dropping opacity, and each of those improvisations is unmeasured.

Start with the pair that has to pass

Choose the background and the body text color first, before any brand color. They are the pair with the strictest requirement (4.5:1 at AA, 7:1 at AAA) and the pair that covers the most pixels, and once they are fixed everything else has room to move.

Give yourself headroom. A pair at exactly 4.6:1 leaves nothing for the caption that ends up slightly lighter or the surface that ends up slightly darker. Aiming at 7:1 for the main pair means the derived states still pass when they drift.

Then pick the surface — the card or panel color that sits on the background. It should be close enough to the background to read as a layer rather than a block, and far enough that its edge is visible without a border. In practice that is one or two steps on a ramp, and the same text color has to clear 4.5:1 against both.

Swatches are not enough

Expand each color into a tonal ramp — 50 through 950, the convention most design systems use — before you decide anything else. A ramp gives you the hover state (one step darker), the border (a mid step), the muted caption (a step that still clears 4.5:1), and the disabled fill, all from inside the hue family instead of from an opacity guess.

Build the ramp in a perceptual space. In HSL, stepping lightness in even increments produces a ramp that bunches up in the yellows and stretches in the blues, because HSL lightness is a geometric artefact rather than a perceptual quantity. In OKLCH the same even steps look even — which is what makes a 50–950 ramp usable as a set of interchangeable steps. See OKLCH vs HSL for the detail.

A useful property falls out of a well-built ramp: on most hues, steps 50–400 carry dark text safely and steps 700–950 carry light text safely, and the 500–600 band carries neither. Knowing where that band sits for your hue tells you immediately which steps can host text and which can only be surfaces.

Assign roles, then test the pairings

Name what each color does before you check anything: background, surface, text, muted text, border, primary, accent, and the three status colors — success, warning, danger. The names are what you will test against, and they are also what you will export as tokens.

Then measure the pairings an interface actually renders:

PairingNeedsBecause
Text on background4.5:1Body copy
Text on surface4.5:1Card and panel copy
Muted text on background4.5:1Captions are still text
Text on primary4.5:1Button labels
Primary on background3:1The button has to be findable
Border on surface3:11.4.11, if the border defines the control
Focus ring on both3:11.4.11 and 2.4.11
Each status color on its own surface4.5:1Error text is text

Grade every pairing in the palette builder

Beyond contrast

Contrast is criterion 1.4.3. Criterion 1.4.1 — "use of color" — is separate and is failed by palettes that pass contrast easily: a status system where red means failed and green means passed is unreadable to someone with deuteranopia if the two are at the same lightness, no matter how well each contrasts with the background.

Three habits cover it:

One more, easy to forget: do not rely on hue alone to distinguish adjacent series in a chart. Vary lightness across the series and the chart survives both color vision deficiency and being printed in greyscale.

A worked example

A light theme built the way described above, starting from the text and background pair:

RoleColorAgainstRatio
Background#FFFFFF
Surface#F4F5F7background1.1:1
Text#16181Dbackground17.6:1
Muted text#5B6070surface6.4:1
Border#C9CDD6surface1.5:1
Primary#1F5FD0background5.9:1
Text on primary#FFFFFFprimary5.9:1

Two things to notice. The border at 1.5:1 against the surface is fine for a decorative divider and not fine as the only thing marking an input field — that case needs a darker border, around #8A90A0, to clear 3:1. And the primary at 5.9:1 does double duty: it passes as a link color on the background and it carries white text as a button fill, which is only possible because it sits below the 500–600 dead band rather than inside it.

What goes wrong

  1. Every color at mid-lightness. A palette of five colors all around L 55% is a palette with no text color and no background.
  2. Opacity as a substitute for a ramp. opacity: .5 produces an unmeasured color that changes with whatever is behind it.
  3. Status colors chosen by hue only. Red, amber and green at the same lightness are one color to a large group of users.
  4. Testing the palette but not the theme. Dark mode is a second palette with its own measurements, not the same one inverted.
  5. Stopping at the swatch strip. Apply the palette to a real layout — a nav, a card, a form, a table — before committing it.

Checklist

Frequently asked questions

How many colors does an accessible palette need?

Fewer than most palettes have. One background, one surface a step off it, one text color, one primary and one accent will build a complete interface — five roles. What an accessible palette needs is not more hues but more steps per hue, so hover states, borders and disabled text come from a ramp rather than being improvised.

Can I use my brand color if it fails contrast?

Yes, on large surfaces. A brand color that fails 4.5:1 as a text color usually passes 3:1 as a button fill or a hero panel, and those are the places a brand color does its work. Derive a darker or lighter step from the same hue for the places it has to carry small text, and the two read as one color to a visitor.

Does an accessible palette have to look muted?

No. Contrast is a function of lightness, and chroma is nearly free — a fully saturated color at the right lightness passes as easily as a desaturated one. Palettes that end up muted usually got there by pushing every color toward mid-lightness, which is the range where both light and dark text fail.

What about color blindness — is contrast enough?

No. Contrast covers WCAG 1.4.3; distinguishability covers 1.4.1, which says color must not be the only means of conveying information. A red and a green at identical lightness pass no contrast test against each other and collapse into the same color under deuteranopia. Test the palette under simulation and pair every color-coded state with text, an icon or a pattern.

Next

Palette builderGenerate a palette and grade every pairing in one place. WCAG color contrastThe thresholds behind every number on this page. Color design tokensTurn the roles above into CSS, Tailwind or DTCG tokens. Palette galleryEighteen palettes with their pass counts already measured.