Color gamut is the complete range of colors that a device, medium or color space can represent, and for web work it usually means comparing sRGB with Display P3. A color that is valid but cannot be produced by a given display is called out of gamut. The standard CSS check is the color-gamut media feature, which accepts srgb, p3 and rec2020.
A gamut is defined by its three primaries plus a white point. The triangle those primaries form on a chromaticity chart contains every color the space can mix. A larger triangle means more saturated colors are possible.
Chromaticity coordinates from the CSS Color 4 spec:
P3 and sRGB share the same blue and white, so the extra range is mostly in saturated reds and greens. In CSS you write wide-gamut colors with color(display-p3 r g b). When a color is outside the screen's gamut, a browser applies gamut mapping. CSS Color 4 defines gamut mapping to an RGB destination as a binary search that reduces chroma in OkLCh, while plain clipping is the cheaper alternative.
In-gamut RGB coordinates are inside 0 to 1. A color is out of gamut for a space when converting into it gives a channel below 0 or above 1.
const inv = m => { const [a,b,c,d,e,f,g,h,i] = m.flat(); const D = a*(e*i-f*h) - b*(d*i-f*g) + c*(d*h-e*g);
return [[(e*i-f*h)/D,(c*h-b*i)/D,(b*f-c*e)/D],[(f*g-d*i)/D,(a*i-c*g)/D,(c*d-a*f)/D],[(d*h-e*g)/D,(b*g-a*h)/D,(a*e-b*d)/D]]; };
const mul = (M, v) => M.map(r => r[0]*v[0] + r[1]*v[1] + r[2]*v[2]);
const xy = (x, y) => [x / y, 1, (1 - x - y) / y];
function toXYZ(p) { const W = xy(0.3127, 0.3290); const P = p.map(([x, y]) => xy(x, y));
const M = [0,1,2].map(r => P.map(c => c[r])); const S = mul(inv(M), W); return M.map(r => r.map((v, j) => v * S[j])); }
const SRGB = toXYZ([[0.64,0.33],[0.30,0.60],[0.15,0.06]]);
const P3 = toXYZ([[0.68,0.32],[0.265,0.69],[0.15,0.06]]);
const dec = v => Math.sign(v) * (Math.abs(v) <= 0.04045 ? Math.abs(v)/12.92 : ((Math.abs(v)+0.055)/1.055)**2.4);
const enc = v => Math.sign(v) * (Math.abs(v) <= 0.0031308 ? 12.92*Math.abs(v) : 1.055*Math.abs(v)**(1/2.4) - 0.055);
const p3ToSrgb = c => mul(inv(SRGB), mul(P3, c.map(dec))).map(enc);
for (const c of [[1,0,0],[0,1,0],[0,0,1],[0.5,0.5,0.5]]) {
const o = p3ToSrgb(c); const ok = o.every(v => v >= -1e-6 && v <= 1 + 1e-6);
console.log('color(display-p3 ' + c.join(' ') + ') -> srgb ' + o.map(v => v.toFixed(4)).join(' ') + (ok ? ' in gamut' : ' OUT of sRGB gamut'));
}
color(display-p3 1 0 0) -> srgb 1.0931 -0.2267 -0.1501 OUT of sRGB gamut
color(display-p3 0 1 0) -> srgb -0.5116 1.0183 -0.3107 OUT of sRGB gamut
color(display-p3 0 0 1) -> srgb -0.0000 0.0000 1.0420 OUT of sRGB gamut
color(display-p3 0.5 0.5 0.5) -> srgb 0.5000 0.5000 0.5000 in gamut
Use the color-gamut media feature: (color-gamut: p3) is true when the browser and output device support approximately the Display P3 gamut or more. The srgb value is true on nearly every color display, and p3 is a superset of srgb. The check is approximate, because a device can claim Rec. 2020 but render much less of it.