X Xerobit

OKLCH Color Space — Perceptually Uniform Colors for CSS

OKLCH is a perceptually uniform color space that makes predictable color palettes, consistent lightness, and accessible contrast easy. Learn the oklch() CSS function, how to...

Mian Ali Khalid · · 4 min read
Use the tool
Color Picker
Pick colors, convert hex/RGB/HSL/OKLCH, and check WCAG contrast.
Open Color Picker →

OKLCH is the best color space for UI design in 2024. Unlike HSL, colors at the same L value actually look equally bright — meaning you can mix colors across a palette without some looking washed out and others looking vibrant.

Pick colors with the Color Picker.

OKLCH syntax

/* oklch(lightness chroma hue) */
oklch(L C H)

/* L: 0 = black, 1 = white (or 0% to 100%) */
/* C: chroma (colorfulness), 0 = gray, ~0.37 = most vivid */
/* H: hue angle 0-360 */

/* Examples: */
oklch(0.5 0.2 250)    /* Medium blue */
oklch(70% 0.15 30)    /* Warm orange */
oklch(0.9 0.05 150)   /* Light green */

/* With alpha: */
oklch(0.5 0.2 250 / 0.5)  /* 50% transparent blue */

Why OKLCH beats HSL

/* HSL "same lightness" problem: */
hsl(0,   100%, 50%)   /* Red — looks very bright */
hsl(240, 100%, 50%)   /* Blue — looks darker than red at "same" 50% L */
hsl(120, 100%, 50%)   /* Green — looks even brighter */

/* HSL lightness is not perceptually uniform.
   Different hues appear to have different brightness at the same value. */

/* OKLCH: same L = actually same perceived brightness */
oklch(0.5 0.2 30)    /* Orange — same perceived brightness as... */
oklch(0.5 0.2 250)   /* Blue — they actually look equally bright */
oklch(0.5 0.2 140)   /* Green — same brightness too */

Building a tonal palette

OKLCH makes it easy to create systematic palettes — just vary lightness:

:root {
  /* Blue palette: same C and H, vary L */
  --blue-50:  oklch(0.97 0.04 250);  /* Very light */
  --blue-100: oklch(0.93 0.07 250);
  --blue-200: oklch(0.87 0.10 250);
  --blue-300: oklch(0.78 0.14 250);
  --blue-400: oklch(0.67 0.18 250);
  --blue-500: oklch(0.56 0.22 250);  /* Base */
  --blue-600: oklch(0.47 0.22 250);
  --blue-700: oklch(0.38 0.18 250);
  --blue-800: oklch(0.30 0.14 250);
  --blue-900: oklch(0.22 0.09 250);
  --blue-950: oklch(0.15 0.05 250);  /* Very dark */
  
  /* Same approach for any hue: just change H */
  --red-500:    oklch(0.56 0.22 30);
  --green-500:  oklch(0.56 0.22 145);
  --purple-500: oklch(0.56 0.22 300);
  
  /* Each at the same L=0.56 looks equally bright! */
}

WCAG contrast with OKLCH

/* APCA contrast: use L for approximate checking */
/* Target: 4.5:1 ratio for normal text (WCAG AA) */

/* Good contrast pairs (L difference ≈ 0.5+): */
background: oklch(0.97 0.04 250);  /* L=0.97 */
color: oklch(0.20 0.15 250);       /* L=0.20 → large L difference */

/* Accessible neutral text: */
:root {
  --text-strong:  oklch(0.15 0.01 265);   /* Near black */
  --text-default: oklch(0.30 0.02 265);   /* Dark gray */
  --text-muted:   oklch(0.50 0.02 265);   /* Gray — check contrast! */
  
  --bg-base:      oklch(1.00 0 0);         /* White */
  --bg-subtle:    oklch(0.97 0.01 265);   /* Off white */
  --bg-muted:     oklch(0.93 0.02 265);   /* Light gray */
}

Chroma limits by hue

Chroma (C) maximum varies by hue because some hue+chroma combinations fall outside the sRGB gamut:

/* Max chroma varies by hue (rough guide): */
/* Red (H≈30):    max ~0.28 */
/* Yellow (H≈90): max ~0.18 */
/* Green (H≈145): max ~0.28 */
/* Cyan (H≈200):  max ~0.32 */
/* Blue (H≈265):  max ~0.32 */
/* Purple (H≈310):max ~0.26 */
/* Magenta(H≈340):max ~0.28 */

/* Safe across all hues: */
/* C ≤ 0.15 — stays in sRGB */

/* High-chroma (P3 gamut): */
/* C ≤ 0.28 — needs color-gamut: p3 check */

@media (color-gamut: p3) {
  :root {
    --accent: oklch(0.6 0.28 250);  /* Vivid P3 blue */
  }
}
/* Fallback (no @media needed — browsers clip to sRGB automatically): */
:root {
  --accent: oklch(0.6 0.22 250);   /* sRGB-safe blue */
}

CSS color-mix() with OKLCH

/* Interpolate in OKLCH for perceptually correct gradients: */
.button {
  background: color-mix(in oklch, var(--blue-500), var(--green-500) 50%);
}

/* Transparent to color (no "gray middle" problem): */
.gradient {
  background: linear-gradient(
    in oklch,
    oklch(0.6 0.3 30),
    oklch(0.6 0.3 250)
  );
  /* Hue travels through yellow/green naturally, not through gray */
}

JavaScript: generate OKLCH palette

function generatePalette(hue, chroma = 0.22, steps = 11) {
  const lightnesses = [0.97, 0.93, 0.87, 0.78, 0.67, 0.56, 0.47, 0.38, 0.30, 0.22, 0.15];
  return lightnesses.slice(0, steps).map((l, i) => ({
    step: i * 100 + 50,
    css: `oklch(${l} ${chroma * (l < 0.2 || l > 0.9 ? 0.5 : 1)} ${hue})`,
  }));
}

generatePalette(250);
// [
//   { step: 50,  css: 'oklch(0.97 0.11 250)' },
//   { step: 150, css: 'oklch(0.93 0.22 250)' },
//   ...
// ]

Browser support

OKLCH works in Chrome 111+, Firefox 113+, Safari 15.4+. For older browsers, provide HSL fallback:

.element {
  color: hsl(240, 60%, 40%);               /* Fallback */
  color: oklch(0.40 0.18 265);             /* Modern browsers */
}

Related posts

Related tool

Color Picker

Pick colors, convert hex/RGB/HSL/OKLCH, and check WCAG contrast.

Written by Mian Ali Khalid. Part of the Frontend & Design pillar.