Why do design-system palette ramps built by stepping HSL lightness look uneven across hues, and what do perceptually uniform spaces like OKLCH fix?
answer
- same number, different brightness
- eye most sensitive to green
- yellow versus blue at fifty percent
- lightness, chroma, hue
- gamut limits vary by hue
basics
~20 sHSL lightness is channel arithmetic that ignores how the eye perceives brightness, so equal HSL steps look unequal across hues. Perceptually uniform spaces such as OKLCH or CIELAB make equal lightness steps look roughly equal and comparable across hues.
solid answer
~50 sHSL is a geometric rearrangement of screen RGB values; its lightness is just the average of the largest and smallest channel, so it treats all hues as equally bright. The eye does not: it is far more sensitive to green than to blue, so pure yellow and pure blue share 50% HSL lightness yet look nothing alike in brightness. A ramp stepped in HSL therefore has uneven jumps, and step six of blue is much darker than step six of yellow. **Perceptually uniform** spaces - CIELAB and the newer OKLab, with its polar form **OKLCH** (lightness, chroma, hue) - are built so equal numeric steps approximate equal perceived differences. Ramps built on shared OKLCH lightness targets look evenly spaced and line up across hues. The catches: maximum chroma varies by hue and lightness, so steps need gamut mapping, and uniformity is only approximate.
code
pseudocode · 16 lineslightnessTargets = [0.97, 0.93, 0.86, 0.77, 0.67, 0.57, 0.48, 0.39, 0.30, 0.22]
function taper(L):
# 1.0 at mid lightness, falling toward the light and dark ends
return max(0.2, 1 - abs(L - 0.6) / 0.6)
function buildRamp(hue, peakChroma):
ramp = []
for L in lightnessTargets:
C = peakChroma * taper(L)
color = perceptualColor(L, C, hue)
while not inDisplayGamut(color):
C = max(0, C - 0.005) # reduce chroma, keep lightness and hue
color = perceptualColor(L, C, hue)
ramp.append(color)
return rampgo deeper
Recall that HSL lightness does not match what the eye sees, so equal HSL steps look uneven, and that perceptual spaces like OKLCH are used to build even ramps.
Explain the mechanism: channel arithmetic versus the eye's unequal sensitivity, the yellow-versus-blue example, and why lightness, chroma and hue are the right controls for a ramp.
Handle the catches in a real palette: gamut mapping when chroma is out of reach, hue drift in CIELAB blues, and keeping contrast rules separate from perceptual lightness.
Decide whether migrating an established palette to a perceptual build is worth it, weighing consistency and repeatability against visible changes to every product surface.
## Why HSL misleads **HSL** (hue, saturation, lightness) is a cylindrical rearrangement of the RGB values a screen uses. Its **lightness** is computed from the channels arithmetically - the average of the largest and smallest of red, green and blue. That formula treats every hue as if it contributed equally to brightness. Human vision does not work that way. The eye is much more sensitive to green light than to red, and much less sensitive to blue. WCAG's own definition of **relative luminance** for sRGB shows the imbalance: it weights the linearised channels as 0.2126 for red, 0.7152 for green and 0.0722 for blue. Apply it to two colors with identical HSL lightness of 50%: - **Pure yellow** (full red plus full green) has a relative luminance of about 0.93. - **Pure blue** has a relative luminance of about 0.07. Same HSL lightness, wildly different brightness. A palette built by stepping HSL lightness in equal increments inherits this: ramps for different hues do not line up, and even within one hue the steps feel uneven, bunching in some ranges and jumping in others. ## What a perceptually uniform space is A **perceptually uniform** color space is designed so that equal numeric distances correspond, approximately, to equal perceived differences. The two in common design-system use: | Space | Components | Notes | |---|---|---| | **CIELAB** (and its polar form LCh) | L lightness, a and b opponent axes, or chroma and hue | The long-standing standard; some hues, notably blues, drift toward purple as lightness or chroma changes at a fixed hue angle | | **OKLab** (and its polar form **OKLCH**) | L lightness, C chroma, H hue angle | A newer space designed to keep hue more stable across lightness and chroma changes | | **HSL** (for comparison) | Hue, saturation, lightness | Not perceptual; convenient arithmetic only | The polar forms are the useful ones for ramps, because they separate the three things a palette designer actually controls: - **Lightness** - how light or dark a step looks. - **Chroma** - how colorful it is, from gray outward. - **Hue** - which color family it belongs to. ## Building ramps in a perceptual space 1. Choose **shared lightness targets**, one per step, used for every hue. 2. For each hue, set a **chroma curve** that peaks around the middle steps and tapers toward the very light and very dark ends. 3. Keep the **hue** fixed, or shift it slightly where a family looks wrong at the extremes. 4. **Map each step into the display gamut** and review the ramps side by side on real screens. The payoff is that step six of teal, red and amber have similar perceived lightness, so the palette feels like one system and a step can be swapped for its neighbour in another hue predictably. ## The catches - **Gamut limits.** Not every combination of lightness, chroma and hue exists on a screen. Maximum chroma varies strongly by hue and lightness - yellows reach high chroma only when light, blues only when fairly dark. A ramp with constant chroma will ask for impossible colors; they must be **gamut-mapped**, usually by reducing chroma while holding lightness and hue. - **Approximate uniformity.** No space matches perception perfectly, so ramps still need review by eye. - **Contrast is a different measure.** WCAG contrast ratios are computed from relative luminance, not from OKLCH or CIELAB lightness, so equal perceptual steps do not give equal contrast ratios. Which step pairs meet contrast is a separate rule set. - **Output is still RGB.** Colors are defined in the perceptual space but exported as the RGB values each platform renders, so the conversion belongs in the palette build, not in hand edits. ## Checking the result A perceptual build is a starting point, not a verdict. Useful checks before publishing the ramps: - **Side-by-side grid** - every hue as a row, steps as columns; each column should look like one lightness band. - **Grayscale test** - convert the grid to grayscale; a column whose cells differ visibly in gray shows a step whose lightness is off. - **Real screens** - apply the steps in actual layouts, because a swatch that looks right alone can look loud or dull in context. - **Neighbour test** - adjacent steps should be distinguishable at a glance, or one of them is not earning its place. None of this is specific to the web: a native mobile palette has the same eye, the same gamut limits and the same need for ramps that line up across hues.
- If OKLCH is better, why do many existing palettes still look fine despite being built in HSL?Experienced designers corrected them by eye, step by step, compensating for what HSL got wrong. The cost shows up later: the corrections are undocumented, new hues do not line up, and regenerating the palette reproduces the original unevenness. A perceptual space makes the intent explicit and repeatable.
- What does it mean that CIELAB blues drift toward purple?In CIELAB, keeping the hue angle fixed while changing lightness or chroma does not keep the perceived hue fixed for some colors; blues in particular start to look purple. A blue ramp generated at one hue angle then looks like two colors. OKLab was designed to reduce this, which is one reason palette builders adopted it.
Stepping HSL lightness is like setting every instrument in a band to the same number on its volume knob: the knobs match, but the trumpet still sounds louder than the flute. A perceptual space calibrates to what the listener hears, not to the knob.
saying these in an interview costs you the question
- Equal HSL lightness means equal perceived brightness
- OKLCH ramps automatically pass WCAG contrast at every step
- Any lightness, chroma and hue combination can be displayed
- Perceptual spaces make visual review of ramps unnecessary
- Holding a CIELAB hue angle fixed always holds the perceived hue