For nearly three decades, digital web design relied almost exclusively on the sRGB color space via traditional hexadecimal (#3b82f6), RGB (rgb(59, 130, 246)), and HSL (hsl(217, 91%, 60%)) color notations. However, modern displays (such as OLED screens and Apple Retina displays) can display up to 25–30% more vibrant colors than standard sRGB can encode.
Furthermore, legacy color models like HSL suffer from severe mathematical flaws in human perceptual uniformity—leading to inaccessible text contrast and muddy color blending.
With the advent of the CSS Color Module Level 4, frontend engineers now have native browser support for OKLCH, OKLab, and Display P3. This guide explores how these color spaces work, why OKLCH is becoming the default choice for design systems (including Tailwind CSS v4), and how to perform accurate conversions.
1. Comparing Core Web Color Models
| Color Space | Functional CSS Syntax | Coordinates & Bounds | Perceptually Uniform? | Wide Gamut? | Primary Web Application |
|---|---|---|---|---|---|
| Hex / sRGB | #3B82F6 or #3B82F6CC |
00–FF per channel (8-bit) |
❌ No | ❌ No (sRGB only) | Legacy CSS, quick styling, tokens |
| RGB / RGBA | rgb(59 130 246 / 0.8) |
0–255 or 0%–100% |
❌ No | ❌ No (sRGB only) | Canvas manipulation, CSS styling |
| HSL / HSLA | hsl(217deg 91% 60%) |
Hue 0–360°, Sat/Light 0–100% |
❌ No | ❌ No (sRGB only) | Intuitive manual color picking |
| OKLCH | oklch(0.65 0.18 250) |
Lightness 0–1, Chroma 0–0.4, Hue 0–360° |
✅ Yes | ✅ Yes (P3 & Rec.2020) | Design systems, accessible palettes |
| OKLab | oklab(0.65 -0.05 -0.15) |
Lightness 0–1, a/b opponent axes |
✅ Yes | ✅ Yes | Gradient interpolation, color blending |
| Display P3 | color(display-p3 0.23 0.51 0.96) |
Red, Green, Blue 0.0–1.0 |
❌ No | ✅ Yes (P3 gamut) | Vivid photography, HDR accents |
2. The Perceptual Uniformity Problem in HSL
The fundamental issue with HSL (Hue, Saturation, Lightness) is that Lightness does not correspond to perceived human brightness.
Human visual photoreceptors are far more sensitive to green and yellow wavelengths than to blue wavelengths. In HSL:
- Pure yellow (
hsl(60, 100%, 50%)) has a perceived relative luminance of ~0.93 (blindingly bright). - Pure blue (
hsl(240, 100%, 50%)) has a perceived relative luminance of ~0.07 (very dark).
Perceived Brightness at HSL Lightness = 50%:
Yellow (Hue 60°): ████████████████████ (Luminance ~0.93)
Cyan (Hue 180°): ███████████████ (Luminance ~0.78)
Green (Hue 120°): ████████████ (Luminance ~0.72)
Red (Hue 0°): ██████ (Luminance ~0.21)
Blue (Hue 240°): ██ (Luminance ~0.07)
Because of this discrepancy, you cannot build programmatic accessible color palettes in HSL simply by holding lightness constant. Setting dark text on hsl(60, 100%, 50%) passes WCAG contrast, but dark text on hsl(240, 100%, 50%) fails catastrophically.
3. How OKLCH Solves Color Consistency
OKLCH separates color into three independent perceptual dimensions:
- Lightness ($L$): From $0.0$ (pure black) to $1.0$ (pure white). An OKLCH lightness of $0.7$ has identical perceived human brightness whether your hue is neon yellow, sky blue, or deep crimson.
- Chroma ($C$): The color's purity or saturation, starting at $0$ (monochrome gray) up to $\sim 0.37+$ (maximum saturation allowed by the display gamut).
- Hue ($H$): The angular wheel position from $0^\circ$ to $360^\circ$ ($0^\circ$ is pinkish red, $90^\circ$ is yellow, $140^\circ$ is green, $240^\circ$ is blue).
Practical Example: Building an Accessible UI Token Scale
With OKLCH, you can create consistent component variants (e.g., info, success, warning, destructive) by locking the Lightness and Chroma and altering only the Hue angle:
:root {
/* All buttons have identical perceived lightness (0.62) and saturation (0.19) */
--btn-primary: oklch(0.62 0.19 255); /* Brand Blue */
--btn-success: oklch(0.62 0.19 145); /* Green */
--btn-warning: oklch(0.62 0.19 85); /* Amber */
--btn-danger: oklch(0.62 0.19 25); /* Red */
/* High-contrast text on any of the above buttons */
--btn-text: #ffffff;
}
Every button variation automatically shares the exact same WCAG contrast ratio against the white text, eliminating visual imbalance.
4. Why OKLab Blending Eliminates the "Gray Dead Zone"
When mixing colors in CSS gradients using standard sRGB, interpolating between complementary colors (like blue and yellow) passes through an unnatural muddy gray or dark brown midpoint:
/* Legacy sRGB Gradient (Muddy Gray Middle) */
.legacy-gradient {
background: linear-gradient(to right, #3b82f6, #f59e0b);
}
/* Modern Perceptual OKLab Gradient (Vibrant, Natural Transition) */
.modern-gradient {
background: linear-gradient(in oklab to right, #3b82f6, #f59e0b);
}
Specifying in oklab tells the browser rendering engine to compute intermediate color steps through the linear OKLab opponent color space, preserving vibrant chromaticity across the entire gradient ramp.
5. Converting Colors Between Formats Programmatically
JavaScript: Hex to sRGB Float Conversion
function hexToRgb(hex: string): { r: number; g: number; b: number; a: number } {
const clean = hex.replace(/^#/, '');
let r = 0, g = 0, b = 0, a = 1;
if (clean.length === 3 || clean.length === 4) {
r = parseInt(clean[0] + clean[0], 16) / 255;
g = parseInt(clean[1] + clean[1], 16) / 255;
b = parseInt(clean[2] + clean[2], 16) / 255;
if (clean.length === 4) {
a = parseInt(clean[3] + clean[3], 16) / 255;
}
} else if (clean.length === 6 || clean.length === 8) {
r = parseInt(clean.slice(0, 2), 16) / 255;
g = parseInt(clean.slice(2, 4), 16) / 255;
b = parseInt(clean.slice(4, 6), 16) / 255;
if (clean.length === 8) {
a = parseInt(clean.slice(6, 8), 16) / 255;
}
}
return { r, g, b, a };
}
sRGB to Linear Luminance for WCAG Contrast Calculation
To calculate WCAG 2.1 relative luminance ($Y$), gamma-encoded 8-bit sRGB channels must first be linearized:
$$\text{channel}_{\text{linear}} = \begin{cases} \frac{c}{12.92} & \text{if } c \le 0.04045 \ \left(\frac{c + 0.055}{1.055}\right)^{2.4} & \text{if } c > 0.04045 \end{cases}$$
Relative luminance is then computed using the ITU-R BT.709 coefficients:
$$Y = 0.2126 \cdot R_{\text{linear}} + 0.7152 \cdot G_{\text{linear}} + 0.0722 \cdot B_{\text{linear}}$$
The contrast ratio between foreground luminance $L_1$ and background luminance $L_2$ (where $L_1 > L_2$) is:
$$\text{Contrast Ratio} = \frac{L_1 + 0.05}{L_2 + 0.05}$$
6. Interactive Tool Integration
To test your brand colors across all 16+ color representations, evaluate real-time WCAG 2.1 AA/AAA compliance, test color blindness simulations, and blend colors in OKLab space, use our free Color Converter Tool.