OKLCH Color Space
The Problem
AI agents almost always generate colors in hex, rgb(), or hsl() format. These older color spaces have a fundamental flaw: they are not perceptually uniform. In HSL, two colors with the same lightness value (e.g., hsl(60, 100%, 50%) yellow and hsl(240, 100%, 50%) blue) appear drastically different in perceived brightness. This makes it nearly impossible to create consistent, accessible color palettes by simply adjusting hue values. AI-generated palettes in HSL often have inconsistent contrast ratios, muddy mid-tones, and colors that "jump" in perceived brightness across the spectrum.
The Solution
OKLCH (oklch()) is a CSS color function based on the Oklab perceptual color model. It uses three components:
- L — Lightness (0% = black, 100% = white), perceptually linear
- C — Chroma (0 = gray, higher = more vivid), represents colorfulness
- H — Hue (0–360 degrees), the color angle on the color wheel
The key advantage: if you keep L constant and change H, the perceived brightness stays the same. This makes palette creation predictable — you can generate a set of colors that look equally bright to the human eye.
Why OKLCH Beats HSL
/* HSL: These "look" like the same lightness, but they're not */
.yellow {
color: hsl(60, 100%, 50%); /* Appears very bright */
}
.blue {
color: hsl(240, 100%, 50%); /* Appears much darker */
}
/* OKLCH: Same lightness = same perceived brightness */
.yellow {
color: oklch(80% 0.18 90); /* Visually bright */
}
.blue {
color: oklch(80% 0.18 264); /* Equally bright */
}
Code Examples
Basic OKLCH Syntax
:root {
/* oklch(lightness chroma hue) */
--brand-primary: oklch(55% 0.25 264); /* Vivid blue */
--brand-secondary: oklch(65% 0.2 150); /* Teal-green */
--brand-accent: oklch(70% 0.22 30); /* Warm orange */
/* With alpha transparency */
--overlay: oklch(20% 0 0 / 0.5); /* Semi-transparent black */
}
Creating a Perceptually Uniform Palette
By fixing lightness and chroma and only rotating hue, every color has the same visual weight:
:root {
/* Categorical palette — all colors appear equally prominent */
--chart-1: oklch(65% 0.2 30); /* Red-orange */
--chart-2: oklch(65% 0.2 90); /* Yellow */
--chart-3: oklch(65% 0.2 150); /* Green */
--chart-4: oklch(65% 0.2 210); /* Cyan */
--chart-5: oklch(65% 0.2 270); /* Blue */
--chart-6: oklch(65% 0.2 330); /* Magenta */
}
Lightness Scale for a Single Hue
:root {
--blue-hue: 264;
--blue-chroma: 0.15;
--blue-50: oklch(97% var(--blue-chroma) var(--blue-hue));
--blue-100: oklch(93% var(--blue-chroma) var(--blue-hue));
--blue-200: oklch(85% var(--blue-chroma) var(--blue-hue));
--blue-300: oklch(75% var(--blue-chroma) var(--blue-hue));
--blue-400: oklch(65% var(--blue-chroma) var(--blue-hue));
--blue-500: oklch(55% var(--blue-chroma) var(--blue-hue));
--blue-600: oklch(45% var(--blue-chroma) var(--blue-hue));
--blue-700: oklch(37% var(--blue-chroma) var(--blue-hue));
--blue-800: oklch(30% var(--blue-chroma) var(--blue-hue));
--blue-900: oklch(22% var(--blue-chroma) var(--blue-hue));
}
Theming with OKLCH Custom Properties
:root {
--hue: 264;
--chroma: 0.2;
--color-primary: oklch(55% var(--chroma) var(--hue));
--color-primary-light: oklch(75% var(--chroma) var(--hue));
--color-primary-dark: oklch(35% var(--chroma) var(--hue));
--color-primary-subtle: oklch(95% 0.03 var(--hue));
--color-surface: oklch(99% 0.005 var(--hue));
--color-text: oklch(20% 0.02 var(--hue));
--color-text-muted: oklch(45% 0.02 var(--hue));
}
/* Change the entire theme by adjusting one variable */
.theme-green {
--hue: 150;
}
.theme-red {
--hue: 25;
}
Accessible Color Pairs
With OKLCH, you can guarantee contrast by controlling the lightness delta:
:root {
/* A lightness difference of ~45-50% in oklch roughly maps to WCAG AA 4.5:1 */
--bg: oklch(97% 0.01 264);
--text: oklch(25% 0.02 264);
--btn-bg: oklch(50% 0.2 264);
--btn-text: oklch(98% 0.01 264);
}
OKLCH vs HSL — Real Comparison
/* Creating "same lightness" grays in HSL — they're not truly equal */
.hsl-problem {
--gray-warm: hsl(30, 10%, 50%);
--gray-cool: hsl(210, 10%, 50%);
/* These two grays have visibly different perceived brightness */
}
/* OKLCH grays are genuinely perceptually matched */
.oklch-solution {
--gray-warm: oklch(55% 0.02 60);
--gray-cool: oklch(55% 0.02 250);
/* These two grays actually look equally bright */
}
Common AI Mistakes
- Defaulting to
hexorhsl()for all color values whenoklch()would produce more consistent palettes - Assuming HSL lightness is perceptually uniform —
hsl(60, 100%, 50%)andhsl(240, 100%, 50%)look vastly different in brightness despite identical lightness values - Using chroma values that exceed the gamut for certain hue/lightness combinations — the browser will clip them, but the result may differ from intent
- Not taking advantage of OKLCH's hue rotation for generating multi-color palettes — AI often hard-codes each color independently instead of rotating hue
- Creating color scales by evenly spacing lightness values (10%, 20%, 30%...) without considering that very high chroma at extreme lightness is out of gamut
- Using
oklch(0% 0 0)andoklch(100% 0 0)for black and white when simplerblackandwhitekeywords suffice
When to Use
- Design system color tokens: OKLCH makes it straightforward to generate consistent lightness scales across different hues
- Data visualization palettes: Categorical colors at the same perceived brightness prevent one color from dominating visually
- Accessible theming: Controlling the lightness delta between background and text ensures predictable contrast
- Dynamic theming: Rotating the hue custom property shifts the entire palette while preserving visual harmony
When to stay with hex/rgb
- When targeting older browsers that don't support OKLCH (pre-2023) and a fallback is impractical
- When interfacing with design tools or APIs that only accept hex or rgb values
- Single-color declarations where perceptual uniformity is irrelevant