AI Overview SummaryWeb styling is moving from HSL to OKLCH. Learn the mathematics of color coordinates, gamma correction, perceptual uniformity, and conversion matrices.
The Evolution of Digital Color
For decades, developers and designers styled the web using simple RGB hexadecimal codes (like #ff0000). Later, CSS Color Module Level 3 introduced HSL (Hue, Saturation, Lightness), making colors readable and easier to manipulate programmatically. However, HSL has a major weakness: it is not perceptually uniform. A yellow color with a lightness of 80% looks vastly brighter to the human eye than a blue color with the exact same lightness.
With the release of CSS Color Module Level 4, modern browsers gained support for OKLCH (Luminance, Chroma, Hue), a color space designed by Björn Ottosson in 2020. OKLCH is perceptually uniform. If you change the Hue of an OKLCH color while keeping Lightness and Chroma constant, the human eye perceives the transition as smooth and uniform, preventing the unexpected brightness changes common to HSL.
In this deep dive, we will explore the mathematical foundations of color representation, trace the conversion formulas from standard sRGB to HSL, and step through the transformation matrix mathematics required to convert sRGB to the perceptually uniform OKLCH space.
1. The sRGB Color Space: The Baseline
The standard RGB (sRGB) color space is a device-dependent model matching how physical screens emit light. An sRGB color is defined by three components: Red ($R$), Green ($G$), and Blue ($B$), mapped between $0$ and $1$ (or $0$ and $255$).
Non-linear vs. Linear RGB (Gamma Correction)
Crucially, sRGB is non-linear. To map digital values efficiently to human vision (which is more sensitive to dark shades than light ones), screens compress colors using a power curve called Gamma Correction.
To perform mathematical operations on sRGB colors (like matrix transformations), we must first convert the non-linear sRGB values ($R_{\text{srgb}}, G_{\text{srgb}}, B_{\text{srgb}}$) into linear RGB values ($R_{\text{linear}}, G_{\text{linear}}, B_{\text{linear}}$). The transformation for a channel $C$ is:
$$C_{\text{linear}} = \begin{cases} \frac{C_{\text{srgb}}}{12.92} & \text{if } C_{\text{srgb}} \le 0.04045 \ \left(\frac{C_{\text{srgb}} + 0.055}{1.055}\right)^{2.4} & \text{if } C_{\text{srgb}} > 0.04045 \end{cases}$$
After processing in linear space, we reverse this calculation (companding) to send the values back to a screen.
2. The Mathematics of sRGB to HSL
HSL is a cylindrical transformation of sRGB. It does not model human vision; it is a mathematical distortion of the RGB cube into a double-cone.
Given $R, G, B \in [0, 1]$:
Step A: Identify Extremes
Find the minimum and maximum channel values, and calculate the range ($\Delta$):
$$C_{\text{max}} = \max(R, G, B)$$
$$C_{\text{min}} = \min(R, G, B)$$
$$\Delta = C_{\text{max}} - C_{\text{min}}$$
Step B: Lightness ($L$)
Lightness is simply the midpoint between the brightest and darkest channels:
$$L = \frac{C_{\text{max}} + C_{\text{min}}}{2}$$
Step C: Saturation ($S$)
Saturation represents the purity of the color relative to its lightness:
$$S = \begin{cases} 0 & \text{if } \Delta = 0 \ \frac{\Delta}{1 - |2L - 1|} & \text{if } \Delta \ne 0 \end{cases}$$
Step D: Hue ($H$)
Hue represents the angle around the color circle, measured in degrees ($[0^\circ, 360^\circ]$):
$$H = \begin{cases} 0^\circ & \text{if } \Delta = 0 \ 60^\circ \times \left( \frac{G - B}{\Delta} \bmod 6 \right) & \text{if } C_{\text{max}} = R \ 60^\circ \times \left( \frac{B - R}{\Delta} + 2 \right) & \text{if } C_{\text{max}} = G \ 60^\circ \times \left( \frac{R - G}{\Delta} + 4 \right) & \text{if } C_{\text{max}} = B \end{cases}$$
The Failure of HSL
Consider HSL yellow hsl(60, 100%, 50%) and HSL blue hsl(240, 100%, 50%). Both have $L = 50%$. However, yellow ($Y = 0.2126R + 0.7152G + 0.0722B$) has a physical luminance of roughly $0.92$, whereas blue has a luminance of only $0.07$. This massive difference causes HSL-based design systems to break when building accessible contrast themes dynamically.
3. The Mathematics of sRGB to OKLCH
OKLCH fixes this by translating colors into a space aligned with human visual perception. Converting sRGB to OKLCH requires transforming the values through intermediate standard color spaces: CIE XYZ (a device-independent reference space) and a specialized LMS cone space (which mimics human retina receptors).
Linear sRGB ---> CIE XYZ ---> LMS ---> Non-linear LMS ---> OKLAB ---> OKLCH
Step 1: Convert sRGB to Linear RGB
Apply the gamma transformation formula described in Section 1 to Red, Green, and Blue.
Step 2: Linear RGB to CIE XYZ
Convert linear RGB to the reference XYZ space using the standard D65 illumination matrix:
$$\begin{bmatrix} X \ Y \ Z \end{bmatrix} = \begin{bmatrix} 0.4124 & 0.3576 & 0.1805 \ 0.2126 & 0.7152 & 0.0722 \ 0.0193 & 0.1192 & 0.9505 \end{bmatrix} \begin{bmatrix} R_{\text{linear}} \ G_{\text{linear}} \ B_{\text{linear}} \end{bmatrix}$$
Step 3: XYZ to LMS (Cone Space)
Map XYZ values to the LMS space using the OKLab optimization matrix:
$$\begin{bmatrix} L \ M \ S \end{bmatrix} = \begin{bmatrix} 0.8189 & 0.3619 & -0.1289 \ 0.0329 & 0.9293 & 0.0361 \ 0.0482 & 0.2644 & 0.6338 \end{bmatrix} \begin{bmatrix} X \ Y \ Z \end{bmatrix}$$
Step 4: Non-linear LMS Transformation
Apply a cube-root compression to mimic the non-linear response of human photoreceptors:
$$L' = L^{1/3}, \quad M' = M^{1/3}, \quad S' = S^{1/3}$$
Step 5: Non-linear LMS to OKLab ($L, a, b$)
Transform the compressed LMS coordinates into the OKLab components: Lightness ($L$), green-red axis ($a$), and blue-yellow axis ($b$).
$$\begin{bmatrix} L_{\text{ok}} \ a_{\text{ok}} \ b_{\text{ok}} \end{bmatrix} = \begin{bmatrix} 0.2104 & 0.7936 & -0.0040 \ 1.9779 & -2.4286 & 0.4506 \ 0.0259 & 0.7828 & -0.8087 \end{bmatrix} \begin{bmatrix} L' \ M' \ S' \end{bmatrix}$$
Step 6: OKLab to OKLCH
Finally, convert the Cartesian coordinates ($a, b$) of OKLab into polar coordinates to get Chroma ($C$) and Hue ($H$):
-
Lightness ($L$): Identical to $L_{\text{ok}}$ (ranges from $0$ to $1$ or $0%$ to $100%$).
-
Chroma ($C$): The Euclidean distance from the center axis, representing saturation/color purity:
$$C = \sqrt{a_{\text{ok}}^2 + b_{\text{ok}}^2}$$
-
Hue ($H$): The polar angle in degrees:
$$H = \operatorname{atan2}(b_{\text{ok}}, a_{\text{ok}}) \times \frac{180}{\pi}$$
If $H < 0$, add $360^\circ$ to normalize the angle to $[0^\circ, 360^\circ]$.
4. Coding the Conversion (JavaScript)
Below is the complete, high-precision JavaScript implementation to convert a standard Hex color to its CSS-ready oklch() equivalent:
/**
* Converts a Hex color string to OKLCH coordinates
* @param {string} hex - Hex color string (e.g., "#3b82f6")
* @returns {string} The CSS oklch() declaration
*/
function hexToOklch(hex) {
// 1. Parse hex to normalized sRGB [0, 1]
const cleanHex = hex.replace('#', '');
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
// 2. Convert to Linear RGB
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
const rL = toLinear(r);
const gL = toLinear(g);
const bL = toLinear(b);
// 3. Linear RGB to LMS (Optimized combined step)
const l = 0.4122214708 * rL + 0.5363113620 * gL + 0.0514459919 * bL;
const m = 0.2119034982 * rL + 0.6806995451 * gL + 0.1073969166 * bL;
const s = 0.0883024619 * rL + 0.2817188376 * gL + 0.6299787005 * bL;
// 4. Non-linear LMS mapping
const l_ = Math.cbrt(l);
const m_ = Math.cbrt(m);
const s_ = Math.cbrt(s);
// 5. LMS to OKLab (L, a, b)
const L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
const a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
const b_axis = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
// 6. OKLab to OKLCH
const chroma = Math.sqrt(a * a + b_axis * b_axis);
let hue = Math.atan2(b_axis, a) * (180 / Math.PI);
if (hue < 0) hue += 360;
// Round values for clean CSS output
const L_rounded = (L * 100).toFixed(1);
const C_rounded = chroma.toFixed(3);
const H_rounded = hue.toFixed(1);
return `oklch(${L_rounded}% ${C_rounded} ${H_rounded})`;
}
// Example usage:
console.log(hexToOklch("#3b82f6")); // Outputs: "oklch(60.1% 0.198 250.7)"
Conclusion
Understanding the math behind color models is essential for building modern web applications. While HSL served its purpose for basic, code-friendly color manipulation, its failure to align with human perception makes it obsolete for advanced UI design systems.
Transitioning to OKLCH ensures that your programmatic contrast adjustments, color scales, and dark-theme generations look perfectly balanced to the human eye, leveraging transformations that bridge digital screens with our biological visual system.
Ready to use the engine?
Deploy our high-precision Design manifest for your professional workload. Fast, free, and privacy-encrypted.
Launch The Tool