#BDE0FE |
#CDB4DB |
#FFC8DD |
#A8DADC |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
๐จ Palette in play โ Summer Gallery ยท
#BDE0FE#CDB4DB#FFC8DD#A8DADC
"The blouse looked mint online and arrived sage."
ย
Colour is one of the top drivers of returns in apparel and home goods, and a chunk of it is not photography โ it's the last few centimetres of the pipeline, where a colour the studio measured carefully gets squeezed into whatever box the shopper's screen provides.
ย
@media (color-gamut) is the CSS tool for that last stretch. It's a small feature with one genuinely confusing behaviour, so let's do it properly.
ย
๐งพ The syntax, and the part that confuses everyone
@media (color-gamut: srgb) { /* ~every colour display in existence */ }
@media (color-gamut: p3) { /* Display-P3 capable or wider */ }
@media (color-gamut: rec2020) { /* Rec. 2020 capable or wider */ }
The values are cumulative, not exclusive. A P3 display matches srgb and p3. A Rec. 2020 display matches all three. There is no "sRGB only" query.
ย
That has one practical consequence, and it's the bug I see most:
- /* WRONG โ the srgb block wins on a P3 display because it comes last */
- @media (color-gamut: p3) { .swatch { background: color(display-p3 0.4 0.85 0.78); } }
- @media (color-gamut: srgb) { .swatch { background: #A8DADC; } }
+ /* RIGHT โ narrow first, wide last, let the cascade upgrade */
+ .swatch { background: #A8DADC; }
+ @media (color-gamut: p3) { .swatch { background: color(display-p3 0.4 0.85 0.78); } }
+ @media (color-gamut: rec2020) { .swatch { background: color(rec2020 0.38 0.86 0.79); } }
Order narrow โ wide. Same rule as min-width breakpoints.
ย
You rarely need (color-gamut: srgb) at all โ a plain declaration outside any query is the sRGB case, and it also covers browsers that don't understand the feature.
ย
๐๏ธ Why an e-commerce team should care
The colours that suffer most in sRGB are exactly the ones product teams argue about: saturated teals, corals, emeralds and true reds. Here's the palette above converted precisely to P3 โ these are the coordinates that reproduce the same colour, before you push anything:
| Product swatch | sRGB | Display-P3 coordinates |
|---|---|---|
|
|
#BDE0FE |
color(display-p3 0.7680 0.8743 0.9844) |
|
|
#CDB4DB |
color(display-p3 0.7877 0.7094 0.8479) |
|
|
#FFC8DD |
color(display-p3 0.9663 0.7927 0.8635) |
|
|
#A8DADC |
color(display-p3 0.6990 0.8493 0.8591) |
For pastels the gain is modest. Do the same exercise with a saturated coral or emerald and the P3 version is visibly closer to the physical sample โ which is the entire business case.
ย
Where it pays in a store:
- Swatch chips next to the photo. If the chip is CSS and the photo is a P3 image, they should live in the same space or the mismatch reads as a defect.
- "Colour: Sage" filters. Ten green variants need to stay distinguishable; extra gamut buys separation.
- Brand-critical packaging shots. The hero colour is the product.
Where it doesn't: price, body copy, chrome, empty states. Don't spend review time there.
ย
๐ฏ The pattern for a product swatch component
swatches.css โ data attributes + one enhancement block
.swatch {
inline-size: 2.25rem;
aspect-ratio: 1;
border-radius: 50%;
border: 1px solid rgb(0 0 0 / 0.12);
background: var(--swatch);
}
[data-colour='powder'] { --swatch: #BDE0FE; }
[data-colour='lilac'] { --swatch: #CDB4DB; }
[data-colour='blush'] { --swatch: #FFC8DD; }
[data-colour='mint'] { --swatch: #A8DADC; }
@media (color-gamut: p3) {
[data-colour='powder'] { --swatch: color(display-p3 0.7680 0.8743 0.9844); }
[data-colour='lilac'] { --swatch: color(display-p3 0.7877 0.7094 0.8479); }
[data-colour='blush'] { --swatch: color(display-p3 0.9663 0.7927 0.8635); }
[data-colour='mint'] { --swatch: color(display-p3 0.6990 0.8493 0.8591); }
}
.swatch[aria-pressed='true'] {
outline: 2px solid CanvasText;
outline-offset: 3px;
}
Two blocks, no per-component duplication, and the selected state uses an outline rather than a colour change โ because colour alone must never be the only signal, gamut or no gamut.
ย
๐ Combining it with everything else
Media features compose, and this is where the pattern earns its keep:
/* wide gamut AND dark mode */
@media (color-gamut: p3) and (prefers-color-scheme: dark) {
:root { --accent: color(display-p3 0.45 0.88 0.85); }
}
/* respect the user who wants less intensity */
@media (color-gamut: p3) and (prefers-contrast: less) {
:root { --accent: #A8DADC; } /* fall back to the calmer sRGB value */
}
/* HDR-capable screens, a separate question from gamut */
@media (dynamic-range: high) {
.hero { background-image: image-set('hero-hdr.avif' type('image/avif')); }
}
dynamic-range is about brightness range; color-gamut is about how many hues. A screen can be one without the other.
ย
๐งโ๐ป Reading it from JavaScript
Useful for analytics โ "what share of my traffic can even see the enhancement?" โ and for canvas work:
const p3 = window.matchMedia('(color-gamut: p3)');
analytics.track('display_gamut', {
gamut: p3.matches ? 'p3' : 'srgb',
hdr: matchMedia('(dynamic-range: high)').matches,
});
// canvas has to opt in explicitly; it clamps to sRGB otherwise
const ctx = canvas.getContext('2d', {
colorSpace: p3.matches ? 'display-p3' : 'srgb',
});
// gamut can change when a laptop is plugged into an external monitor
p3.addEventListener('change', (e) => rerenderSwatches(e.matches));
That last listener matters more than it sounds. Drag a browser window from a MacBook screen to a cheap external monitor and the match result flips mid-session.
ย
If colour-related returns are meaningfully lower on P3 sessions after you ship the enhancement, you have your answer โ and a number to take to whoever funds the work. ย ๐ Measuring whether it moved the needle
// tag the gamut on colour-related events so returns data can be sliced by it
const gamut = matchMedia('(color-gamut: p3)').matches ? 'p3' : 'srgb';
analytics.track('variant_selected', { sku, colour: 'mint', gamut });
analytics.track('return_initiated', { sku, reason: 'colour_mismatch', gamut });
ย
๐ง Four traps
1 ยท The cascade order bug. Covered above; it silently disables your enhancement.
ย
2 ยท Assuming P3 means "accurate". A wide-gamut panel that is uncalibrated is wide and wrong. Enhancement improves the ceiling, not the floor.
ย
3 ยท Photos and CSS drifting apart. Product images are usually tagged sRGB. If your CSS chips jump to P3 and the photo doesn't, the chip stops matching the garment.
ย
4 ยท Reviewing only on Apple hardware. Your design team's screens are the least representative sample of your customers' screens.
ย
๐งพ The checklist
- Plain declaration = sRGB. Add
p3, thenrec2020, in that order. - Never use
(color-gamut: srgb)to undo an enhancement โ it matches P3 screens too. - Convert exactly first; push chroma only if the product genuinely needs it.
- Keep CSS swatches and product photography in the same colour space.
- Selection state uses shape or outline, never colour alone.
- Listen for
changeโ the gamut can flip when a monitor is plugged in. - Tag analytics with the gamut so you can prove the impact.
Getting the sRGB baseline right is still the biggest win, and starting from a palette that's already balanced saves the argument โ ColorFiind publishes each set with its HEX codes ready to convert.
ย
Browse palettes for your product swatches
ย
Do you know what share of your traffic is on a P3 display? It's a five-line analytics change to find out. ๐




Top comments (0)