The Color Problem No One Talks About
You define a color palette. Primary blue, secondary purple, neutral grays. You apply it across your UI. Everything looks balanced—until you add yellow.
Suddenly yellow buttons feel louder than blue ones, even though both are set to the same "saturation." Text on yellow backgrounds is harder to read than text on blue, even though both pass WCAG contrast checks in your design tool.
This is not a design mistake. It is a limitation of how we define colors.
Most design systems use HSL (Hue, Saturation, Lightness) or RGB (Red, Green, Blue). These color models are not perceptually uniform—meaning a change in numeric value does not produce a consistent change in perceived brightness or vibrancy.
OKLCH solves this. It is a perceptually uniform color space designed to match how humans actually see color. When you adjust lightness in OKLCH, the perceived brightness changes consistently across all hues. When you set chroma (saturation), colors feel equally vibrant.
Modern design systems—Tailwind CSS v4, Radix Colors, shadcn/ui—are switching to OKLCH. Here is why you should too.
What is OKLCH?
OKLCH stands for OK Lightness Chroma Hue. It is a cylindrical representation of the Oklab color space, which was designed by Björn Ottosson to be perceptually uniform and computationally efficient.
How OKLCH Works
OKLCH has three components:
- L (Lightness) – 0 (black) to 1 (white), perceptually uniform
- C (Chroma) – 0 (grayscale) to ~0.4 (highly saturated), represents vibrancy
- H (Hue) – 0° to 360°, same circular hue wheel as HSL
Example:
/* OKLCH: oklch(L C H) */
color: oklch(0.65 0.2 250);
0.65= Lightness (65% brightness)0.2= Chroma (moderately saturated)250= Hue (blue-purple range)
Why "Perceptually Uniform" Matters
HSL is not perceptually uniform. Two colors with the same lightness value can look drastically different in brightness:
/* Both are HSL lightness 50%, but perceived brightness differs */
hsl(60, 100%, 50%) /* Yellow – looks bright */
hsl(240, 100%, 50%) /* Blue – looks darker */
Yellow at 50% lightness looks much brighter than blue at 50% lightness because human vision is more sensitive to yellow wavelengths.
OKLCH is perceptually uniform. Two colors with the same OKLCH lightness have the same perceived brightness:
/* Both have OKLCH lightness 0.7 – perceived brightness is equal */
oklch(0.7 0.15 100) /* Yellow */
oklch(0.7 0.15 250) /* Blue */
This makes color systems predictable. You can generate shades programmatically and trust they will look balanced.
OKLCH vs HSL vs RGB: Side-by-Side
| Feature | RGB | HSL | OKLCH |
|---|---|---|---|
| Perceptually Uniform | ❌ No | ❌ No | ✅ Yes |
| Predictable Lightness | ❌ No | ❌ No | ✅ Yes |
| Easy to Reason About | ❌ No | ✅ Yes | ✅ Yes |
| Consistent Saturation | ❌ No | ❌ No | ✅ Yes |
| Browser Support | ✅ Full | ✅ Full | ⚠️ Modern (polyfill available) |
| Design Tool Support | ✅ Full | ✅ Full | ⚠️ Limited (improving) |
Why Design Systems Are Switching to OKLCH
1. Predictable Color Scales
Design systems need color scales—light to dark variations of a base color. With HSL, adjusting lightness produces uneven results:
/* HSL: adjusting lightness by fixed steps */
hsl(220, 80%, 90%) /* Very light blue */
hsl(220, 80%, 70%) /* Light blue */
hsl(220, 80%, 50%) /* Medium blue */
hsl(220, 80%, 30%) /* Dark blue */
Visually, the steps from 90% → 70% and 70% → 50% do not feel equal. The perceived brightness change is inconsistent.
With OKLCH, equal lightness steps produce equal perceived brightness steps:
/* OKLCH: perceptually uniform steps */
oklch(0.9 0.1 250) /* Very light blue */
oklch(0.7 0.1 250) /* Light blue */
oklch(0.5 0.1 250) /* Medium blue */
oklch(0.3 0.1 250) /* Dark blue */
Each step feels visually balanced. This is critical for auto-generating themes or accessible color ramps.
2. Accessible Contrast Without Trial-and-Error
WCAG contrast ratios are based on perceptual lightness, not HSL values. OKLCH's lightness channel aligns with WCAG calculations, making it easier to predict contrast:
- Text needs a contrast ratio of 4.5:1 (AA) or 7:1 (AAA)
- In OKLCH, a lightness difference of ~0.4 usually hits AA contrast
- In HSL, lightness difference does not map cleanly to contrast ratios
Example: Creating accessible text on a blue background.
HSL approach (trial-and-error):
background: hsl(220, 80%, 50%);
color: hsl(220, 80%, 95%); /* Check contrast – might fail */
OKLCH approach (predictable):
background: oklch(0.5 0.15 250);
color: oklch(0.95 0.02 250); /* Lightness delta = 0.45 → likely passes AA */
You still need to verify contrast, but OKLCH gives you a better starting point.
3. Consistent Saturation Across Hues
HSL saturation does not feel consistent across hues. 100% saturation yellow looks more vibrant than 100% saturation blue:
hsl(60, 100%, 50%) /* Yellow – feels very bright */
hsl(240, 100%, 50%) /* Blue – feels less vibrant */
OKLCH chroma is perceptually uniform. Setting chroma to 0.2 produces similar vibrancy across all hues:
oklch(0.65 0.2 60) /* Yellow */
oklch(0.65 0.2 240) /* Blue */
/* Both feel equally saturated */
This makes it easy to build color palettes where all accent colors have equal visual weight.
4. Better Gradients and Transitions
CSS gradients in RGB or HSL can produce muddy mid-tones:
/* HSL gradient – muddy middle */
background: linear-gradient(90deg,
hsl(0, 100%, 50%), /* Red */
hsl(240, 100%, 50%) /* Blue */
);
OKLCH gradients interpolate through perceptually uniform space, producing cleaner transitions:
/* OKLCH gradient – smooth, vibrant */
background: linear-gradient(90deg in oklch,
oklch(0.6 0.25 30), /* Red */
oklch(0.6 0.25 270) /* Blue */
);
The intermediate colors look more vibrant and natural.
Migrating Your Design System to OKLCH
Step 1: Convert Existing Colors
Use a tool to convert your current palette:
- OKLCH Color Picker – Visual converter
- Colorjs.io – Batch conversion
Example conversion:
/* Before (HSL) */
--primary: hsl(220, 90%, 56%);
/* After (OKLCH) */
--primary: oklch(0.6 0.22 250);
Step 2: Build Perceptually Uniform Scales
Instead of hardcoding shades, generate them programmatically:
function generateScale(baseL, baseC, baseH, steps = 9) {
const lightnesses = [0.95, 0.85, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25, 0.15];
return lightnesses.map((l, i) => ({
name: `scale-${i + 1}`,
value: `oklch(${l} ${baseC} ${baseH})`,
}));
}
const blueScale = generateScale(0.6, 0.2, 250);
Output:
--blue-1: oklch(0.95 0.2 250); /* Lightest */
--blue-5: oklch(0.55 0.2 250); /* Base */
--blue-9: oklch(0.15 0.2 250); /* Darkest */
Each step has equal perceived brightness differences.
Step 3: Use CSS Color-Mix for Variants
CSS color-mix() works beautifully with OKLCH:
:root {
--primary: oklch(0.6 0.22 250);
}
.button-hover {
background: color-mix(in oklch, var(--primary), black 10%);
}
.button-active {
background: color-mix(in oklch, var(--primary), black 20%);
}
Mixing in OKLCH space produces perceptually accurate results (unlike mixing in RGB, which can shift hues unexpectedly).
Step 4: Ensure Browser Support
OKLCH is supported in:
- ✅ Chrome 111+
- ✅ Safari 15.4+
- ✅ Firefox 113+
For older browsers, use a PostCSS plugin:
npm install @csstools/postcss-oklab-function
/* Before PostCSS */
color: oklch(0.6 0.2 250);
/* After PostCSS (fallback added) */
color: rgb(66, 135, 245);
color: oklch(0.6 0.2 250);
OKLCH in Design Tools
Figma and Sketch do not natively support OKLCH yet. Workaround:
- Define colors in OKLCH in your codebase
- Use a plugin to sync tokens to Figma (e.g., Figma Tokens)
- Or define semantic tokens in Figma, then convert to OKLCH in code
Example workflow:
Figma: Define semantic names (primary, neutral-5)
Code: Convert to OKLCH:
export const colors = {
primary: 'oklch(0.6 0.22 250)',
neutral: {
5: 'oklch(0.55 0.02 270)',
},
};
Real-World Example: Tailwind CSS v4
Tailwind CSS v4 uses OKLCH internally for its color palette. Here is how they define blue:
--blue-500: oklch(0.6 0.22 250);
--blue-600: oklch(0.52 0.22 250);
--blue-700: oklch(0.44 0.22 250);
Notice:
- Lightness decreases linearly (0.6 → 0.52 → 0.44)
- Chroma stays constant (0.22)
- Hue stays constant (250)
Result: Perceptually uniform blue shades that feel evenly spaced.
Common Questions
Does OKLCH support transparency?
Yes. Use the slash syntax:
color: oklch(0.6 0.2 250 / 0.8); /* 80% opacity */
Can I animate OKLCH colors?
Yes. CSS transitions work:
.button {
background: oklch(0.6 0.2 250);
transition: background 0.2s;
}
.button:hover {
background: oklch(0.5 0.25 250);
}
What about P3 or other wide-gamut colors?
OKLCH supports P3 and Rec. 2020 gamuts. Just ensure values are within the displayable range (chroma > ~0.35 may exceed sRGB).
When NOT to Use OKLCH
OKLCH is excellent for design systems, but there are edge cases:
- Legacy browser support required – Stick with RGB/HSL, or use PostCSS fallbacks
- Exact color matching to brand guidelines – If your brand mandates specific hex codes, convert them but verify visually
- Designing for print (CMYK) – OKLCH is display-focused; print workflows still use CMYK
Conclusion
OKLCH is not a trend. It is a better foundation for color systems.
Why switch:
- Predictable lightness and saturation across all hues
- Easier to generate accessible color scales
- Cleaner gradients and color mixing
- Future-proof (browsers, frameworks adopting it)
How to start:
- Convert your existing palette to OKLCH
- Generate scales using perceptual lightness steps
- Use
color-mix()for hover/active states - Add PostCSS fallbacks for older browsers
Your design system will be more consistent, more accessible, and easier to maintain—whether you are designing manually or generating UI with AI.
Color is one of the hardest parts of design systems to get right. OKLCH makes it predictable.
Want a design system with OKLCH built in?
FramingUI ships with perceptually uniform color tokens, ready for modern browsers and AI code generation.