How to Design an Accessible Color Palette in HSLuv for Digital Products

HSLuv lets you build a palette where "same lightness" actually looks like the same lightness, which makes it far easier to hit WCAG contrast targets than with HSL or RGB. Use it when you need a systematic palette for a design system — a set of shades and tints that stay perceptually even, plus semantic tokens you can test in both light and dark themes. The workflow below assumes you can pick a base hue and have a way to generate and check colors (a color tool or a small script); the contrast checks themselves are the standard WCAG ratios, not something HSLuv changes.

Why HSLuv instead of HSL or RGB

HSL is not perceptually uniform. In HSL, hsl(60, 100%, 50%) (yellow) and hsl(240, 100%, 50%) (blue) share the same lightness value but look wildly different in brightness. That means a "50% lightness" scale built in HSL produces shades that don't read as evenly spaced, and contrast against text becomes unpredictable.

HSLuv fixes this by defining lightness (L) to match human perception, while keeping the familiar hue/saturation model. Its key properties for palette work:

  • L is perceptually consistent — two colors with the same L look equally light/dark, regardless of hue.
  • Hue stays stable — you can shift saturation or lightness without the hue drifting.
  • Gamut-aware — HSLuv maps to sRGB, so colors you define are displayable (unlike raw CIELUV/LCH, which can fall outside sRGB).

The trade-off: HSLuv's saturation is not the same as HSL's, and very high saturation at extreme lightness may clip. For UI palettes this is rarely a problem.

Step 1: Choose a base hue

Pick one hue (0–360) as your brand/primary anchor. In HSLuv, hue is the same angle convention as HSL, so you can reason about it the same way:

Hue range Family
0–30 red / orange
30–90 yellow / yellow-green
90–180 green / teal
180–270 cyan / blue
270–330 purple / magenta
330–360 pink / red

Set your base at a saturation you can sustain across the scale — starting around S 60–100 for a vivid primary is typical. Keep the hue fixed for the whole ramp.

Step 2: Generate shades and tints at consistent lightness

Build your ramp by varying L only, holding H and S constant. A practical scale uses roughly even L steps, for example:

L: 95, 88, 78, 66, 54, 44, 34, 26, 18

Each step is a token (primary-100 … primary-900). Because L is perceptual, the visual jump between steps stays even — something HSL can't guarantee.

Two adjustments worth knowing:

  • Lower saturation as you approach the extremes. At L 95 or L 18, high saturation looks neon or muddy. Dropping S by 10–30 points at the ends keeps the ramp usable.
  • Keep hue constant unless you deliberately want a hue shift (some systems rotate hue slightly across a ramp for warmth/coolness; that's a stylistic choice, not a requirement).

Step 3: Check contrast before assigning roles

Generate the ramp first, then test — don't design roles and hope contrast passes.

WCAG contrast requirements you'll be checking against:

Content Minimum ratio (AA) Enhanced (AAA)
Normal body text 4.5:1 7:1
Large text (≥18pt / 14pt bold) 3:1 4.5:1
UI components & graphical objects 3:1 —

For each text/background pair you intend to use, compute the contrast ratio. If a pair fails, adjust L first (it moves contrast most predictably), then S. Because HSLuv's L is perceptual, a fixed L difference between text and background tends to produce a more consistent contrast ratio across hues than the same L difference in HSL.

Step 4: Assign semantic tokens

Map ramp steps to roles rather than using raw color names. A minimal token set:

  • background — page background (usually a very light or very dark neutral)
  • surface — cards, panels (slightly offset from background)
  • text-primary / text-secondary — body and muted text
  • primary / primary-hover — brand actions
  • border — dividers and outlines
  • success / warning / error — status colors

Define each token by its HSLuv value so the relationship is explicit, e.g. text-primary = L 20, background = L 98 → a large L gap that reliably clears 4.5:1.

Step 5: Test light and dark themes

A palette that passes in light mode often fails in dark mode if you just invert it. Instead, rebuild the ramp for dark mode with its own L targets:

  • Dark backgrounds sit around L 12–20; light-mode backgrounds around L 95–98.
  • Text in dark mode needs L high enough to clear 4.5:1 against the dark surface — often L 85+.
  • Saturated colors (primary, error) usually need lower saturation in dark mode to avoid glare, and sometimes a slight L bump.

Check every semantic pair in both themes. Common failures: secondary text too close to background, borders below 3:1, and status colors that pass on white but not on a dark surface.

Common pitfalls

  • Assuming HSLuv guarantees accessibility. It makes consistent lightness easy; it does not enforce contrast. You still measure ratios.
  • Reusing one ramp for both themes. Light and dark need separate L targets.
  • Ignoring non-text contrast. Borders, icons, and focus rings need 3:1 too.
  • Over-saturating extremes. High S at L 95 or L 15 looks wrong; pull saturation down at the ends.
  • Skipping the check step. Always verify final token pairs, not just the ramp.

The result is a palette where lightness is predictable across hues, contrast is verified against WCAG, and roles are defined as tokens — so the system holds up when you add themes or new components.

colorca.org
Design an accessible color palette for digital products in HSLuv color space
geenes.app
Create a color scale in seconds, then export it to sketch or code.
paletton.com
In love with colors, since 2002. A designer tool for creating color combinations that work together well. Formerly known as Color Scheme Designer. Us…