Why do so many color models exist?
Color is simultaneously a physical, perceptual and digital phenomenon — and that triple nature is exactly why so many models exist. Physically, color is a wavelength of light between roughly 380 nm (violet) and 700 nm (red). What the human eye perceives depends on three types of photoreceptor cells called cones: L-cones (sensitive to red-orange), M-cones (green) and S-cones (blue-violet). This biological reality underlies trichromacy — the principle that any visible color can be decomposed into three components.
The problem is that the perceptual color space humans experience — the CIE 1931 chromaticity diagram, defined by the International Commission on Illumination — is not mathematically "clean". It is asymmetric, non-linear and difficult to manipulate directly. That is why engineers created alternative models suited to different tasks:
- RGB / HEX: designed for screens, mirroring the way R, G, B LEDs work.
- HSL / HSV: designed for humans, who think in terms of "hue + intensity + brightness".
- OKLCH / OKLAB: designed for perceptual uniformity, grounded in actual human vision science.
- CMYK: designed for print — out of scope for the web.
In 2026, CSS supports all of these natively. The choice is no longer constrained by browser support but by context: accessibility requirements, perceptual quality, or alignment with a design system. This guide gives you the tools to choose intelligently.
HEX and RGB: the web's historical pair
How HEX works
The hexadecimal format #RRGGBB is probably the most widespread color notation on the web. Each pair of characters represents one 8-bit channel (0–255) in base 16. So #FF5733 breaks down as R=255, G=87, B=51 — a vivid orange. The shorthand #RGB is an alias: #F53 is equivalent to #FF5533.
Since CSS Colors Level 4, alpha is natively supported: #RRGGBBAA, where the final two characters define opacity. #FF573380 gives the same orange at 50% opacity. Compact for design tokens, but not particularly readable for the human developer.
The rgb() function in CSS
The rgb() function exposes the same three channels with a more expressive syntax:
/* Modern syntax (CSS Color Level 4) */
color: rgb(255 87 51); /* orange */
color: rgb(255 87 51 / 50%); /* semi-transparent orange */
/* Legacy comma syntax */
color: rgb(255, 87, 51);
color: rgba(255, 87, 51, 0.5);
The limitations of RGB/HEX
RGB is a perceptually non-uniform space. In practical terms: a +10 shift on the R channel does not produce the same perceived change as a +10 shift on the B channel. Violet is inherently perceived as darker than yellow, even if their RGB-derived luminance values are mathematically equal. This asymmetry makes RGB poorly suited for:
- Creating gradients that look visually uniform.
- Generating coherent color variants (lighter, darker) without hue drift.
- Computing readability contrast (which requires relative luminance — see the WCAG section).
HSL and HSV: designed for human brains
The HSL model explained
HSL (Hue, Saturation, Lightness) arrived in CSS around 2010 and was the first widely-supported "human-friendly" color format. It operates on three axes:
- Hue: an angle from 0 to 360° on the color wheel. 0°/360° = red, 120° = green, 240° = blue.
- Saturation: 0% = neutral grey, 100% = fully saturated, vivid color.
- Lightness: 0% = black, 50% = pure color, 100% = white.
/* HSL examples */
color: hsl(220 70% 50%); /* medium blue */
color: hsl(220 70% 80%); /* pale blue */
color: hsl(220 70% 20%); /* deep dark blue */
color: hsl(220 70% 50% / 30%); /* semi-transparent blue */
The pattern of "fix the hue, vary the lightness" is very natural for designers: to build a monochromatic palette, lock H and S and only adjust L. This is far more intuitive than tweaking three RGB channels simultaneously.
HSV: the drawing software variant
HSV (Hue, Saturation, Value) resembles HSL but replaces Lightness with Value. The practical difference: in HSV, V=100% gives the pure saturated color, while in HSL, L=50% is the pure color. HSV is used in most color pickers (Photoshop, Figma, Sketch) because it maps to how an artist thinks: "start from the pure color and add black (shade) or white (tint)".
The fundamental flaw in HSL
HSL's core problem is that its Lightness axis does not correspond to perceived luminance. Two colors with L=50% — for example hsl(60 100% 50%) (bright yellow) and hsl(240 100% 50%) (bright blue) — are mathematically at the same "lightness" in HSL, but yellow is perceived as dramatically brighter than blue. This produces two concrete problems:
- HSL gradients go muddy: a gradient from yellow to violet in HSL passes through desaturated greens and greys because the intermediate L values don't map to uniform perceived luminance.
- Contrast calculations are wrong: you cannot rely on L to evaluate the accessibility of a text/background combination. You need the relative luminance formula.
The rise of OKLCH and perceptually uniform spaces
What is a perceptually uniform space?
A perceptually uniform color space is engineered so that equal mathematical distances between two points correspond to equal perceived color differences. The CIE Lab space (1976) was the first serious attempt. OKLAB (2020), created by Björn Ottosson, improves on CIE Lab for problematic edge cases — particularly around blues and purples. OKLCH is simply OKLAB expressed in polar coordinates (Lightness, Chroma, Hue), which is more intuitive for humans to work with.
OKLCH syntax in CSS
OKLCH has been natively supported in CSS since 2023:
/* oklch(lightness chroma hue) */
color: oklch(65% 0.15 220); /* medium blue */
color: oklch(85% 0.08 220); /* pale blue */
color: oklch(30% 0.20 220); /* deep dark blue */
color: oklch(65% 0.15 220 / 50%); /* semi-transparent blue */
/* The HSL muddy-gradient problem, solved */
background: linear-gradient(
in oklch,
oklch(65% 0.18 30), /* orange */
oklch(65% 0.18 270) /* violet */
);
/* Result: a gradient through vivid reds and pinks,
with no grey mud in the middle */
color-mix() and perceptual interpolation
The color-mix() function, standardized in CSS Color Level 5, allows mixing two colors in any color space:
/* Mix red and blue in OKLCH */
color: color-mix(in oklch, red 60%, blue);
/* Create a tint of a brand color */
color: color-mix(in oklch, var(--brand-color) 80%, white);
/* Darken a color */
color: color-mix(in oklch, var(--brand-color) 70%, black);
Adam Argyle (Google Chrome team) has extensively documented why color-mix(in oklch, ...) produces perceptually more natural results than sRGB or HSL interpolation. The reason: transitions pass through vivid intermediate colors instead of washed-out or desaturated zones.
Why 2026 is the turning point
For years, OKLCH was confined to CSS experiments. In 2026, three factors converge to make it a production-ready choice:
- Browser support exceeds 93% (Chrome 111+, Firefox 113+, Safari 15.4+).
- Figma and other design tools now export in OKLCH.
- Major design systems (Radix Colors, Open Props) ship their palettes in OKLCH.
sRGB, Display P3, Rec.2020: understanding gamuts
What is a color gamut?
A gamut is the full set of colors that a device can reproduce. It's typically visualized as a triangle overlaid on the CIE 1931 chromaticity diagram. The larger the triangle, the more colors it covers. sRGB, created by HP and Microsoft in 1996, is the web's historical gamut — it covers roughly 35% of colors visible to the human eye.
Display P3, originally developed by Digital Cinema Initiatives and later adopted by Apple, covers approximately 50% more colors than sRGB. Reds are more vivid, greens deeper, blues more saturated. Since the iPhone 7 (2016) and the P3 Retina MacBook Pro (2016–2017), most Apple devices support P3. Samsung Galaxy flagships, Google Pixel 6+ and most modern high-end displays do too.
Targeting P3 screens in CSS
/* Method 1: the color() function */
color: color(display-p3 0.8 0.2 0.1); /* vivid P3 red */
/* Method 2: @media color-gamut with sRGB fallback */
.accent {
color: #e63946; /* sRGB fallback */
}
@media (color-gamut: p3) {
.accent {
color: color(display-p3 0.90 0.18 0.24); /* more vivid red for P3 */
}
}
/* Method 3: OKLCH with out-of-sRGB colors */
/* Chroma values above ~0.37 exceed the sRGB gamut
and display correctly only on P3 screens */
color: oklch(55% 0.28 27); /* ultra-saturated red */
What happens if the screen doesn't support P3?
The browser applies gamut mapping: it projects the out-of-gamut color onto the nearest color within the supported gamut. This isn't catastrophic — users on sRGB screens see a slightly less vibrant version. That's why CSS fallbacks matter. Rec.2020 is the target gamut for 4K TVs and covers about 73% of the visible spectrum — very few screens render it correctly in 2026, but it will become the standard in coming years.
Accessibility: WCAG and contrast ratios
WCAG 2.x contrast requirements
The W3C defines minimum contrast ratios between text and background in the WCAG (Web Content Accessibility Guidelines). These ratios are computed from relative luminance, not raw RGB values:
- AA — Normal text (below 18pt / 14pt bold): minimum ratio 4.5:1.
- AA — Large text (18pt or above / 14pt bold): minimum ratio 3:1.
- AA — UI components (input borders, icons): minimum ratio 3:1 against their background.
- AAA — Normal text: minimum ratio 7:1.
- AAA — Large text: minimum ratio 4.5:1.
How to compute relative luminance
The relative luminance of an RGB color is calculated as follows (WCAG 2.x formula):
/* Step 1: linearize each channel */
function linearize(c) {
c = c / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
/* Step 2: compute luminance */
function relativeLuminance(r, g, b) {
return 0.2126 * linearize(r)
+ 0.7152 * linearize(g)
+ 0.0722 * linearize(b);
}
/* Step 3: compute contrast ratio */
function contrastRatio(l1, l2) {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
Our color converter runs this calculation automatically: enter two colors and get the WCAG contrast ratio with an AA/AAA pass/fail indicator.
APCA: the successor to WCAG 2.x
WCAG 2.x has known blind spots. Most notably, it underestimates contrast for dark text on light backgrounds and can pass combinations that are genuinely hard to read. The APCA (Advanced Perceptual Contrast Algorithm), developed by Andrew Somers and planned for WCAG 3.0, addresses these issues. It factors in font size, weight, and usage type (body text vs. UI label). APCA is not yet mandatory, but design teams increasingly use it alongside WCAG 2.x checks. Tools like the APCA Contrast Calculator and the Figma A11y plugin incorporate it.
Common contrast mistakes
Frequent errors still seen in 2026 production sites:
- Light grey on white:
#999999text on#FFFFFFbackground yields a ratio of only 2.85:1, well below the required 4.5:1. Yet it is widely used for placeholders and secondary labels. - Blue on dark blue:
#0000FFon#0000AAis unreadable even if both "look blue". - White text on yellow: white on
#FFFF00gives only 1.07:1 — nearly indistinguishable. - Confusing contrast with legibility: passing WCAG AA doesn't guarantee that a thin 10px font is readable — size and weight matter as much as the ratio.
Color blindness and inclusive design
The numbers
About 8% of men and 0.5% of women have some form of color vision deficiency. For a site with 100,000 monthly visitors split 50/50 by gender, that's roughly 4,000 people who may struggle to distinguish certain colors. Too many to ignore.
The three main types
- Deuteranopia / deuteranomaly: deficiency in the green channel. The most common (about 5% of men). Red and green are confused.
- Protanopia / protanomaly: deficiency in the red channel (about 1% of men). Same red/green confusion, plus reduced perceived luminance for reds.
- Tritanopia / tritanomaly: deficiency in the blue channel (about 0.01% of the population). Blue and yellow are confused.
- Achromatopsia: complete absence of color perception. Extremely rare (<0.003%). Vision is entirely in greyscale.
Testing your interface
Three free, effective tools:
- Chrome DevTools: Rendering panel (three dots > More Tools > Rendering) > "Emulate vision deficiencies". Simulates deuteranopia, protanopia, tritanopia and achromatopsia live on any page.
- Sim Daltonism (macOS / iOS): a small floating lens that simulates various deficiency types across everything on your screen. An indispensable tool for designers.
- Coblis (coblis.com): upload a screenshot and receive simulations for all deficiency types.
Practical guidelines
The golden rule is to never use color as the sole carrier of information. Concrete examples:
- A red/green bar chart showing success/failure: add a texture or pattern on top of the color.
- A form that marks required fields in red only: add an asterisk (*) and explanatory text.
- A green/red status indicator: add an icon (checkmark/cross) or a text label ("Active" / "Inactive").
- Generated QR codes must maintain strong contrast between the dark pattern and the light background — our QR code generator automatically uses black on white to guarantee the minimum contrast needed for reliable scanning.
Color in programming: conversions and pitfalls
Converting HEX to HSL
The HEX → HSL conversion is a two-step operation: first HEX → RGB (trivial), then RGB → HSL (more involved). Here is the complete algorithm:
function hexToHsl(hex) {
// 1. Parse HEX
let r = parseInt(hex.slice(1, 3), 16) / 255;
let g = parseInt(hex.slice(3, 5), 16) / 255;
let b = parseInt(hex.slice(5, 7), 16) / 255;
// 2. Find min/max
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic (grey)
} 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)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
}
Gamma correction and the sRGB space
The sRGB color space is not linear. RGB values stored in image files or defined in CSS are encoded with a gamma curve (approximately γ=2.2). This means an RGB value of 128 does not represent 50% of physical luminance — it represents roughly 21.6%. For any luminance calculation (such as WCAG contrast), you must first linearize the values, as shown in the algorithm above. Skipping this step produces incorrect contrast ratios — a common bug in handwritten accessibility checkers.
Rounding errors in round-trip conversions
Floating-point conversions accumulate rounding errors. A round-trip RGB → HSL → RGB does not always return the exact original RGB values, especially for neutral greys where the HSL hue is mathematically undefined and arbitrarily set to 0. This instability is irrelevant for display, but can cause bugs in automated tests. Best practice: store colors in their source format and only convert at the point of display or calculation — never through repeated round-trips.
Alpha normalization
Alpha can be expressed three ways depending on context: as a decimal (0–1) in JavaScript and modern CSS functions, as a percentage (0%–100%) in CSS Level 4 syntax, and as an integer (0–255) in the #RRGGBBAA format. Confusing these notations is a classic source of bugs. An alpha of 0.5 in JavaScript is not the same as AA in HEX (which equals 0.667). Use clearly named utility functions — parseAlphaDecimal(), parseAlphaHex() — to avoid this class of error.
Color documentation and Markdown
When documenting a color palette in a README or design spec, formatting matters. A tool like our Markdown to HTML converter lets you preview fenced code blocks with CSS syntax highlighting and render color palette tables cleanly. No specialized tooling required — a fenced block with the css language tag is all you need to make a shareable, readable palette snippet.
Practical color workflow in 2026
Building an accessible palette with Radix Colors
Radix Colors (radix-ui.com/colors) is an open palette system built by the Radix UI team. Each color has 12 steps, with steps 11 and 12 guaranteed to meet WCAG AA contrast on a white background. Palettes are available in sRGB and P3, with pre-calculated dark-mode variants. It is the most solid foundation for building a design system in 2026 without fighting contrast math on every pair.
Tailwind CSS also ships a broad palette (50 through 950 across 11 steps), but without guaranteed accessibility for every text/background combination — you still need to verify each pair manually.
CSS Custom Properties for palettes
The modern way to manage a color palette in CSS is through custom properties (CSS variables):
:root {
/* Base palette in OKLCH */
--color-brand-500: oklch(55% 0.20 250);
--color-brand-300: oklch(75% 0.14 250);
--color-brand-700: oklch(38% 0.22 250);
/* Semantic layer */
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-bg-primary: var(--color-neutral-50);
--color-accent: var(--color-brand-500);
}
[data-theme="dark"] {
--color-text-primary: var(--color-neutral-50);
--color-text-secondary: var(--color-neutral-400);
--color-bg-primary: var(--color-neutral-950);
--color-accent: var(--color-brand-300);
}
Dark mode and contrast
Dark mode is not about inverting colors. Mechanically inverting RGB values produces unexpected results: a vivid blue in light mode might become a muddy orange in dark mode. The right approach is to define two sets of semantic tokens (as above) and verify that every text/background combination passes WCAG AA in both modes. OKLCH makes this straightforward: adjusting only lightness (L) while keeping the same hue (H) and chroma (C) guarantees perceptually coherent variation across themes.
Design tokens and build pipelines
Design tokens are the abstraction layer between design decisions and their implementation. Tools like Style Dictionary (Amazon) or Theo transform a JSON token file into CSS custom properties, Sass variables, JavaScript constants, Swift and Kotlin — a single source of truth for all platforms. The 2026 trend is to define primitive tokens in OKLCH and let the build pipeline generate HEX fallbacks for legacy browsers.
Summary and practical tool
Web color has always been a deeper subject than it appears. What looks like "picking a nice blue" actually involves the physics of light, the biology of human vision, the mathematics of color spaces and the legal constraints of accessibility. In 2026, good practice distills to this:
- Use OKLCH to define and manipulate colors in CSS — clean gradients, perceptually coherent lightness steps.
- Keep a HEX/RGB fallback for legacy browsers (<5% of traffic).
- Verify WCAG AA contrast (4.5:1) systematically for every piece of text.
- Test with color blindness simulation — at minimum deuteranopia and protanopia.
- Never use color as the only information channel.
- Consider the P3 gamut for accent colors on modern devices.
Our color converter lets you switch instantly between HEX, RGB, HSL and OKLCH, compute the WCAG contrast ratio between two colors, and export values in the format that fits your workflow. No installation, no tracking, privacy-first.