If you have ever tried to pick or tweak colors for a web user interface, you know the frustration.
You copy a hex code like #6366f1 from a Figma design file. Then you want to create an accessible button hover state that is “just a little bit lighter,” so you start randomly tweaking hex digits by trial and error.
Before you know it, your vibrant indigo button turned into a muddy, dull grayish-purple mess.
With modern CSS Color Module Level 4 and Level 5 specifications introducing wide-gamut color spaces (display-p3) and perceptually uniform color functions like oklch(), web designers and frontend engineers have access to better color modeling tools than ever before.
In this exhaustive, practical guide, we will explore the physics and mathematics of digital color: from additive RGB light mixing to intuitive HSL cylindrical wheels and modern OKLCH perceptual color spaces.
+------------------------------------+
| THE 4 WEB COLOR SPACES |
+-----------------+------------------+
|
┌──────────┬──────────┬────────┴─┬──────────┬──────────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ HEX │ │ RGB │ │ HSL │ │ OKLCH │
│#6366f1│ │99,102,│ │239,84%│ │0.58, │
│ │ │ 241 │ │ 67% │ │0.21,278│
└───────┘ └───────┘ └───────┘ └───────┘
The Four Core Web Color Models
1. Hexadecimal (HEX)
Hex codes represent 8-bit red, green, and blue light intensity channels in base-16 (0 to 9 and A to F):
# 63 66 f1
── ── ──
R G B
- Red:
0x63= 99 (38.8%) - Green:
0x66= 102 (40.0%) - Blue:
0xF1= 241 (94.5%)
In modern CSS Color 4, 8-digit hex codes support an alpha opacity channel: #6366f180 represents 50% opacity (0x80 = 128 / 255).
2. RGB (Red, Green, Blue)
The direct additive light model used by computer hardware and monitors:
/* Standard sRGB integer notation */
color: rgb(99, 102, 241);
/* Modern CSS Color 4 whitespace syntax with alpha */
color: rgb(99 102 241 / 0.5);
3. HSL (Hue, Saturation, Lightness)
Invented in the 1970s to make color manipulation intuitive for human designers:
- Hue (0 to 360 degrees): 0 is Red, 120 is Green, 240 is Blue.
- Saturation (0% to 100%): Color intensity (0% is grayscale gray, 100% is vibrant pure color).
- Lightness (0% to 100%): 0% is pure black, 50% is standard color, 100% is pure white.
color: hsl(239deg 84% 67%);
4. OKLCH (Perceptually Uniform Color Space)
The modern gold standard for UI design systems. Created in 2020 by Ottosson:
- L (Perceived Lightness):
0(pure black) to1(pure white). - C (Chroma): Purity and saturation (typically
0to0.4). - H (Hue Angle):
0to360.
color: oklch(0.58 0.21 278);
+-----------+-----------------------+---------------------------------------+
| Format | Syntax Example | Best Use Case |
+-----------+-----------------------+---------------------------------------+
| HEX | #6366f1 or #6366f180 | Legacy web styles & brand hex codes |
| RGB | rgb(99 102 241 / 0.8) | Direct hardware / canvas pixel math |
| HSL | hsl(239 84% 67%) | Basic hue shifting in CSS |
| OKLCH | oklch(0.58 0.21 278) | Accessible UI palettes & dark modes |
+-----------+-----------------------+---------------------------------------+
The Perceptual Flaw of HSL vs OKLCH
Why did the W3C introduce OKLCH if we already had HSL?
Because HSL ignores how the human eye perceives brightness.
Human eyes have three types of cone photoreceptors that are far more sensitive to green and yellow light wavelengths than deep blue wavelengths.
In HSL:
- Pure Yellow:
hsl(60, 100%, 50%)has 50% Lightness. - Pure Blue:
hsl(240, 100%, 50%)has 50% Lightness.
In HSL math, both colors are considered “equally bright.” But to a human, pure yellow looks blindingly radiant, while pure blue looks dark! If you place white text over both colors, the white text is readable over blue, but completely invisible over yellow, failing WCAG 2.2 accessibility standards.
OKLCH fixes this entirely:
In OKLCH, oklch(0.70 0.20 90) (yellow) and oklch(0.70 0.20 270) (blue) have the exact same perceived luminance to human retinas. Contrast ratios against black and white remain identical across all hue shifts!
/* Automated accessible button theme in OKLCH */
:root {
--btn-bg: oklch(0.60 0.20 250);
--btn-hover: oklch(0.68 0.20 250); /* Exactly 8% brighter across all hues! */
}
Convert and inspect color spaces in real time with our Color Converter Tool.
Wide Gamut Colors: sRGB vs Display P3
Traditional web colors were constrained to the sRGB gamut established in 1996 for CRT monitors.
Modern OLED smartphones, MacBook Retina screens, and HDR televisions support Display P3, which offers a 25% wider color spectrum with neon greens, deep emeralds, and fiery crimson reds that cannot physically exist in sRGB:
@supports (color: color(display-p3 1 1 1)) {
.vibrant-banner {
background: oklch(0.65 0.30 145); /* Vivid P3 Emerald Green! */
}
}
Format and organize your CSS design tokens with our CSS Formatter Tool.
Conclusion: Master Your Modern UI Color Palette
Adopting modern color spaces like OKLCH makes building accessible, harmonious color palettes and automated dark modes predictable and mathematical.
Convert, tweak, and explore color formats in real time with the TextSorter Color Converter. Everything runs 100% locally in your browser.
Deep Dive: Color Contrast and WCAG 2.2 Accessibility Compliance
Web Content Accessibility Guidelines (WCAG 2.2) mandate strict contrast ratios between text and background colors:
- Level AA (Normal Text): Minimum contrast ratio of 4.5:1.
- Level AA (Large Text 18pt+ / 14pt Bold): Minimum contrast ratio of 3.0:1.
- Level AAA (Enhanced): Minimum contrast ratio of 7.0:1.
Calculating Relative Luminance
Relative luminance (Y) is calculated in linear sRGB space:
Luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Using OKLCH makes meeting these ratios effortless because the Lightness channel (L) maps directly to perceived human brightness.
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.
Deep Architectural Breakdown: The Mathematics of Color Gamuts and CIE LAB
To understand modern digital color spaces, we have to look at the CIE 1931 XYZ Color Space established by the International Commission on Illumination.
Human eyes perceive color through three types of retinal cone cells:
- L-Cones: Sensitive to long wavelengths (Red light)
- M-Cones: Sensitive to medium wavelengths (Green light)
- S-Cones: Sensitive to short wavelengths (Blue light)
The CIE XYZ color space maps all visible colors into a three-dimensional coordinate system.
Why OKLCH Outperforms sRGB and HSL
In 2020, Björn Ottosson developed the Oklab color space using modern perceptual datasets:
- L (Lightness): Matches human perception of surface brightness.
- a and b (Opponent Color Axes): Represent green-red and blue-yellow color opponent axes.
- OKLCH (Cylindrical form of Oklab): Converts
aandbinto polar coordinates: Chroma (C) for colorfulness and Hue (H) for color angle.
Because OKLCH is perceptually uniform, transitioning between colors in a CSS linear gradient avoids the muddy grayish middle zone that plagues standard RGB transitions:
/* Smooth, vibrant perceptual gradient in modern CSS */
.hero-gradient {
background: linear-gradient(in oklch to right, oklch(0.6 0.25 140), oklch(0.6 0.25 280));
}
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.
Extended Technical Deep Dive: CSS Color-Mix() and Dynamic Theming
With modern CSS Color Level 4, frontend developers can blend colors dynamically in any color space using color-mix():
/* Accessible dynamic badge color */
.badge-subtle {
background: color-mix(in oklch, var(--primary) 15%, transparent);
color: var(--primary);
border: 1px solid color-mix(in oklch, var(--primary) 30%, transparent);
}
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.
Complete Color Conversion Algorithms in Pure JavaScript
Let us look at the mathematical formulas required to convert between HEX, RGB, and HSL:
1. Hex to RGB
function hexToRgb(hex) {
let cleanHex = hex.replace(/^#/, '');
if (cleanHex.length === 3) {
cleanHex = cleanHex.split('').map(c => c + c).join('');
}
const num = parseInt(cleanHex, 16);
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255
};
}
2. RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // Grayscale
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.
Deep Architectural Breakdown: Perceptual Color Spaces and Color Blindness Accessibility
Over 8% of men and 0.5% of women experience some form of color vision deficiency (such as Protanopia, Deuteranopia, or Tritanopia).
When designing digital interfaces, relying solely on red and green color hues to communicate success or error states causes critical accessibility barriers for color-blind users.
Designing Accessible Interfaces with Perceptual Luminance:
- Combine Color with Icons: Always pair color changes with visual icons (like a checkmark for success or an alert triangle for errors).
- Maintain 3:1 Contrast Across Luminance Channels: In OKLCH, ensure that error red (
oklch(0.60 0.25 25)) and success green (oklch(0.60 0.20 140)) maintain adequate lightness contrast against dark and light background surfaces.
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.
Real-World Case Studies: How OKLCH Saved Modern Design Systems
Case Study 1: The Invisible Dark Mode Button Crisis
A fintech banking app implemented an automated dark mode theme by inverting HSL lightness values (100% - Lightness). While standard blue and green buttons adapted cleanly, the brand’s primary warning yellow button (hsl(45, 100%, 50%)) inverted into a dull dark mustard yellow that was completely illegible against dark slate gray cards. By migrating their design system tokens to OKLCH perceptual lightness (oklch(0.75 0.18 85)), button contrast ratios remained constant across both light and dark themes.
Case Study 2: The Muddy CSS Gradient Glitch
A SaaS company designed a vibrant hero banner transitioning from bright cyan to neon magenta. When rendered in standard RGB space, the center of the gradient turned into an unattractive grayish purple. Refactoring the CSS to use linear-gradient(in oklch to right, cyan, magenta) produced a radiant, smooth transition without color desaturation in the middle.
Comprehensive CSS Color Token Reference Table
+---------------------+---------------------------+---------------------------------------+
| Color Token Name | Modern OKLCH Value | Fallback HEX / RGB Value |
+---------------------+---------------------------+---------------------------------------+
| --color-primary-500 | oklch(0.58 0.21 260) | #4f46e5 / rgb(79 70 229) |
| --color-primary-600 | oklch(0.51 0.21 260) | #4338ca / rgb(67 56 202) |
| --color-success-500 | oklch(0.65 0.18 145) | #16a34a / rgb(22 163 74) |
| --color-warning-500 | oklch(0.75 0.18 85) | #d97706 / rgb(217 119 6) |
| --color-error-500 | oklch(0.58 0.22 25) | #dc2626 / rgb(220 38 38) |
+---------------------+---------------------------+---------------------------------------+
Convert, tweak, and explore color formats in real time with the TextSorter Color Converter and CSS Formatter.
Extended Technical Deep Dive: Building a Complete Design System Palette Generator
Here is a pure JavaScript module that generates a complete 10-step accessible color scale from any base color in OKLCH:
function generateAccessibleScale(baseHue, chroma = 0.18) {
const lightnessLevels = [0.95, 0.88, 0.80, 0.70, 0.60, 0.50, 0.40, 0.30, 0.20, 0.12];
const scale = {};
lightnessLevels.forEach((l, idx) => {
const step = (idx + 1) * 100;
scale[step] = `oklch(${l.toFixed(2)} ${chroma.toFixed(2)} ${baseHue})`;
});
return scale;
}
console.log(generateAccessibleScale(260)); // Generates beautiful, balanced 100-1000 indigo palette
Convert colors and build accessible palettes with our Color Converter Tool and CSS Formatter.