CSS color gamut: sRGB, Display P3, Rec2020, and OKLCH compared
For most of the web’s history, CSS colors were locked to sRGB (hex / rgb / hsl). With CSS Color Module Level 4 widely shipped and Display P3 ubiquitous on modern devices, wider gamuts are finally available. This article maps the gamut sizes, the CSS syntax for each, browser/display support, and where OKLCH fits in.
What “gamut” means
A color gamut is the set of colors a color space can represent. Wider gamut → more vivid colors, but only if the display can show them.
Approximate coverage on the CIE 1931 chromaticity diagram (rough percentages of the visible spectrum):
| Gamut | Coverage | Used by |
|---|---|---|
| sRGB | 35% | classic web, HD video |
| Display P3 | 45% | iPhone, Mac, modern phones |
| Rec2020 | 75% | UHD/4K HDR video |
| Rec2100 | same as Rec2020 | HDR10, HLG |
These are approximate fractions of the full visible spectrum each space can express. No consumer display reproduces full Rec2020, but Display P3 is now standard on Apple devices and most flagship Android phones.
CSS syntax
Legacy: sRGB-locked
color: #ff0000;
color: rgb(255, 0, 0);
color: hsl(0, 100%, 50%); All sRGB. Cannot express Display P3 reds — value 255 caps you at sRGB’s gamut boundary.
color() function: explicit color space
color: color(display-p3 1 0 0);
color: color(rec2020 1 0 0); color() takes a color-space identifier. display-p3 (1, 0, 0) is a more saturated red than sRGB (1, 0, 0).
oklch(): perceptually uniform
color: oklch(70% 0.2 30); OKLCH (Oklab in polar coordinates) is a perceptually uniform color space.
L(lightness): 0% – 100%.C(chroma / saturation): 0 – ~0.4.H(hue): 0deg – 360deg.
OKLCH is a representation, not a gamut. With high C, OKLCH values can land outside sRGB into Display P3 or Rec2020 territory.
display-p3 keyword (CSS Color 5)
color: rgb(from #ff0000 r g b / display-p3); CSS Color 5’s from-syntax color-space conversion exists in the spec but ships unevenly. Don’t depend on it yet.
Why OKLCH is the recommended default
hsl(0deg, 100%, 50%) reads naturally but lightness is not consistent across hues:
hsl(0deg, 100%, 50%) /* pure red */
hsl(60deg, 100%, 50%) /* yellow — appears blindingly bright */
hsl(240deg, 100%, 50%) /* pure blue — almost black */ HSL’s “50% lightness” is a math value; perceived brightness varies enormously by hue. OKLCH fixes this:
oklch(70% 0.2 0) /* 70% lightness red */
oklch(70% 0.2 60) /* 70% lightness yellow — visually equal weight */
oklch(70% 0.2 240) /* 70% lightness blue — same */ A “lightness 70%, hue varies” set produces visually-balanced colors. Designing palettes by hand becomes practical.
Display reality
| Device | Gamut | Notes |
|---|---|---|
| iPhone 7+ | Display P3 | nine years of consecutive support |
| MacBook 2015+ | Display P3 | iMac 5K and onward |
| iPad Pro | Display P3 | iPad Air 4+ also |
| Flagship Android (Galaxy S, Pixel) | Display P3 | varies by manufacturer |
| High-end PC monitors | Display P3 / Adobe RGB | Eizo, Apple Studio Display |
| Mainstream PC monitors | sRGB (70-99%) | many 4K panels still sRGB-only |
When the display lacks Display P3 support, browsers gamut-map the P3 color to the nearest sRGB representation. The color shows, but the saturation gain is lost.
color-gamut media query
Branch on display capability:
.brand-red {
color: #ff0000;
}
@media (color-gamut: p3) {
.brand-red {
color: color(display-p3 1 0 0);
}
}
@media (color-gamut: rec2020) {
.brand-red {
color: color(rec2020 1 0 0);
}
} “Show the saturated P3 red on P3 displays; fall back to sRGB hex elsewhere.”
Use-case guidance
Brand and accent colors
Use OKLCH or Display P3 with an sRGB fallback:
.btn-primary {
background: #c84b1f; /* sRGB fallback */
background: color(display-p3 0.78 0.3 0.13); /* P3 boost */
} CSS uses the last understood declaration, so browsers that support color() get P3; older ones get the sRGB hex.
Whole design system
Standardize on OKLCH with CSS custom properties:
:root {
--color-red-500: oklch(60% 0.2 25);
--color-red-400: oklch(70% 0.18 25);
--color-red-600: oklch(50% 0.2 25);
} Even lightness steps across the palette become trivial to maintain. Hand-designing Tailwind-like 50/100/…/900 palettes becomes feasible.
Photos and video
Embed color profiles in the asset, not the CSS. Browsers honor the ICC profile and render correctly per display.
Browser support (2026)
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
color(display-p3 ...) | ✓ (111+) | ✓ (113+) | ✓ (15+) |
color(rec2020 ...) | ✓ (111+) | ✓ (113+) | ✓ (15+) |
oklch() / oklab() | ✓ (111+) | ✓ (113+) | ✓ (15.4+) |
@media (color-gamut) | ✓ | ✓ | ✓ |
color-mix() | ✓ (111+) | ✓ (113+) | ✓ (16.2+) |
All major browsers support the core features. IE / legacy is unsupported and assumed dropped.
Implementation pitfalls
1. sRGB values on P3 displays may shift
Some browsers extend sRGB values into P3 gamut on P3 displays automatically. Colors look more saturated than authored. Specify the color space explicitly when the exact appearance matters.
2. Screenshots lose Display P3
Pasting a P3-colored screenshot into a tool that handles sRGB only causes the colors to shift. This is a tooling/color-management issue, not a CSS bug.
3. Print is sRGB-or-CMYK territory
A vivid Display P3 red can’t be printed faithfully. If brand colors must work in print, define them in Adobe RGB or CMYK and use sRGB for the web port.
Summary
CSS color went from sRGB-locked to color() + oklch() with explicit gamuts. The 2026 default is brand colors in Display P3 with sRGB fallback, design system tokens in OKLCH, fallback hex everywhere. @media (color-gamut) lets you ship the saturation boost only where the display can render it. Older monitors get the safe sRGB rendering, new ones get the vivid one.
For converting between hex / rgb / OKLCH and inspecting gamut boundaries, the color converter tool covers the standard cases.