iKit
Guide · 9 min read ·

OKLCH Color Ramp: Build a Perceptually Uniform Scale (2026)

Build an OKLCH color ramp where every step looks evenly spaced. A practical 2026 guide to lightness stepping, chroma tapering, and gamut safety.

OKLCH Color Ramp: Build a Perceptually Uniform Scale (2026)

OKLCH Color Ramp: Build a Perceptually Uniform Scale

Pick a brand color, then make nine shades of it in HSL and the middle ones jump while the extremes barely move. The scale looks lumpy because HSL lightness is not tied to what your eye sees. An OKLCH color ramp fixes this: because OKLCH lightness is calibrated to human perception, stepping it evenly produces a scale that actually looks evenly spaced. This guide shows how to build one, keep it inside the display gamut, and generate it without uploading anything.

TL;DR

  • OKLCH lightness is perceptual, so even L steps look evenly spaced.
  • Fix hue, step lightness in equal intervals, taper chroma at the ends.
  • High chroma at very light or dark L falls outside sRGB and gets clipped.
  • Nine to eleven steps (a 50–950 scale) covers almost every UI need.
  • Generate and preview the whole ramp in the browser, no server upload.

What a perceptually uniform color ramp is

A color ramp is a set of tints and shades built from a single hue: the light greys you use for backgrounds, the mid-tones for borders, the dark ones for text. A perceptually uniform ramp is one where the visual gap between step 3 and step 4 looks the same as the gap between step 7 and step 8. That evenness is what makes a UI feel deliberate instead of accidental.

Why do my HSL color shades look uneven

HSL defines lightness relative to the color signal, not relative to perception. Fully saturated yellow at hsl(60 100% 50%) looks far brighter than blue at hsl(240 100% 50%), even though both claim 50% lightness. Björn Ottosson demonstrated exactly this when he introduced Oklab in December 2020: a constant-lightness HSV hue sweep shows yellow, magenta, and cyan reading much lighter than red and blue. Step lightness evenly in HSL and your eye sees the steps bunch up in the bright half and stretch out in the dark half.

The three axes: lightness, chroma, hue

OKLCH is the cylindrical form of the Oklab color space, exposing three channels: L for perceived lightness (0 to 1, or 0% to 100%), C for chroma or "amount of color" (0 upward, rarely past about 0.4 in practice), and H for hue angle (0 to 360 degrees). Per the MDN reference, the L in oklch() is the perceived lightness we see with our eyes, which is a different quantity from the L in hsl(). That single distinction is why OKLCH is the right tool for ramps.

Where OKLCH came from

Ottosson built Oklab to solve the operations software actually performs on color: darkening, saturating, and generating palettes. He optimized it against perceptual datasets so that lightness, chroma, and hue behave as independent axes. It has since become the interpolation default for gradients in Photoshop and shipped in CSS as part of Color Module Level 4. When you move only the L value, hue and chroma hold steady, which is precisely what a clean ramp needs.

How to build a color ramp in OKLCH

The recipe is three moves: lock the hue, step the lightness, and manage the chroma. You can do this by hand in CSS custom properties or generate it programmatically.

Step 1: fix the hue, step the lightness

Choose your hue angle once and reuse it for every step. Then spread lightness across the range you need. For a nine-step scale, evenly spaced L values from roughly 0.97 down to 0.30 give you a light-to-dark run:

:root {
  --indigo-hue: 264;
  --indigo-50:  oklch(0.97 0.02 var(--indigo-hue));
  --indigo-100: oklch(0.93 0.04 var(--indigo-hue));
  --indigo-300: oklch(0.80 0.11 var(--indigo-hue));
  --indigo-500: oklch(0.62 0.19 var(--indigo-hue));
  --indigo-700: oklch(0.48 0.16 var(--indigo-hue));
  --indigo-900: oklch(0.34 0.10 var(--indigo-hue));
}

Because the L values march down in even steps, the perceived jumps between shades stay consistent. Notice the hue is a shared variable, so retheming the whole ramp is a one-line change.

Step 2: taper the chroma at the ends

Constant chroma across the whole ramp looks wrong at the extremes: a near-white with full chroma turns pastel-dirty, and a near-black with full chroma muddies. Push chroma toward zero at both ends and let it peak in the middle, where the color has room to be saturated. This bell-shaped chroma curve is the single biggest quality lever after even lightness.

A 10-step example scale

Here is a 10-step indigo ramp at hue 264. Lightness descends in even increments; chroma rises to a mid-scale peak then falls:

Step Lightness (L) Chroma (C) Typical use
50 0.97 0.02 Page background
100 0.93 0.04 Subtle fill
200 0.87 0.07 Hover fill
300 0.80 0.11 Border
400 0.71 0.15 Disabled text
500 0.62 0.19 Primary accent
600 0.55 0.19 Accent hover
700 0.48 0.16 Body link
800 0.41 0.13 Heading
900 0.34 0.10 High-contrast text

You can paste any of these values into the iKit Color Picker & Converter to see the swatch, grab a hex fallback, and confirm the shade reads the way you expect before committing it to a token file.

Why do my OKLCH colors look washed out or clipped

OKLCH can name colors that no screen can show. When you request more chroma than the display supports, the browser has to pull the color back into range, and if it does that badly the result looks flat or shifts hue.

How browsers gamut-map out-of-range chroma

The CSS Color 4 specification defines a gamut-mapping algorithm that works in OKLCH: it runs a binary search on chroma, holding lightness and hue fixed, to find the most saturated value that still fits the target gamut. That is far better than naive per-channel clipping, which pulls a bright saturated color down to something much darker and off-hue. The key insight is that reducing chroma preserves the two things your ramp depends on — lightness and hue.

Keeping a ramp inside sRGB and P3

High-chroma values fail most often at the light and dark ends of the scale, which is another reason the tapered chroma curve matters. A few practical rules keep a ramp safe:

  • Keep chroma below about 0.16 when L is above 0.90 or below 0.35.
  • Let chroma peak around L 0.55 to 0.65, where the gamut is widest.
  • If you target Display-P3, you can push mid-scale chroma higher, but always test the sRGB fallback.
  • When a swatch looks duller than its neighbours, it is usually being clipped, not miscalculated.

color-mix() vs hand-tuned stops

You can interpolate between two anchor colors instead of specifying every stop. color-mix(in oklch, ...) blends in the perceptual space, so a mix of white and your accent passes through even, believable intermediates rather than the muddy purple you get mixing in sRGB:

.tint-20 {
  background: color-mix(in oklch, var(--indigo-500) 20%, white);
}
.shade-20 {
  background: color-mix(in oklch, var(--indigo-500) 80%, black);
}

color-mix() is fast to write, but hand-tuned stops give you full control over the chroma curve. Many teams use color-mix() for a first draft, then adjust the extreme steps by hand.

How many steps should a color ramp have

Nine to eleven steps is the sweet spot for a UI palette. The Tailwind-style 50–950 scale is the de facto standard because it maps cleanly to real roles: 50–200 for surfaces, 300–400 for borders and muted text, 500–600 for accents, 700–950 for text and emphasis.

Generating the scale without a server

You do not need a cloud tool to compute a ramp. A short loop produces the L values and hands the rest to CSS:

const hue = 264;
const steps = 9;
const ramp = Array.from({ length: steps }, (_, i) => {
  const L = 0.97 - (i * 0.67) / (steps - 1);
  const C = 0.19 * Math.sin((i / (steps - 1)) * Math.PI);
  return `oklch(${L.toFixed(2)} ${C.toFixed(2)} ${hue})`;
});
console.log(ramp.join("\n"));

The sin() term gives you the bell-shaped chroma curve for free. Everything runs locally, which matters when the color is an unreleased brand asset. If you would rather not write code, the iKit Color Palette Generator builds and previews a full scale in the browser, and the Gradient Generator uses the same OKLCH interpolation for smooth backgrounds.

Checking contrast at each step

An even-looking ramp is not automatically an accessible one. Before you ship, verify that text steps meet WCAG contrast against their intended backgrounds. Because OKLCH lightness is perceptual, two steps with a large L gap will usually pass, but the middle of the scale is where pairs sneak under the 4.5:1 threshold. Check the exact pairs you actually use rather than assuming.

Reusing one ramp across themes

Once the L and C curves are dialled in, you can spin up a second hue by changing a single number, and the new ramp inherits the same perceptual spacing. That is the real payoff of building in OKLCH: the structure is portable across every color in your system, so a five-hue palette is five hue angles sharing one lightness-and-chroma recipe.

References

Related on iKit

Related posts