Colors on the web are on the rise. Soon, we’ll be able to escape the sRGB prison and use more colors than ever, by tapping into color spaces such as display-p3
. Safari already supports most of the new color functions and spaces, and support in Chrome is on its way.
Say you want to use these richer colors on devices that support them? How exactly would you do that? Earlier today I whipped together two patterns to achieve this. Let’s take a look.
~
# Table of Contents
~
# Hotter than hotpink
My favorite color to debug is hotpink
. But with display-p3
we can have an even hotter pink if we wanted, namely color(display-p3 1 0 0.87)
To make our dev-lives more easier, we could put it into a custom property:
--hotterpink: color(display-p3 1 0 0.87);
To use it, we simply need to refer to its name:
:root {
--hotterpink: color(display-p3 1 0 0.87);
background: var(--hotterpink); /* 🟠 Nice, but no fallback value … */
}
But what about browsers that don’t support display-p3
? How can we make those browsers fall back to our “plain” hotpink?
Using a fallback value with the var()
function won’t work because CSS does not check the validity of the custom property that’s being referred to – it only checks if a value (other than the guaranteed invalid value initial
) has been set or not. If one is set, it won’t use the fallback.
:root {
background: var(--hotterpink, hotpink); /* ❌ Does not work */
}
So, what now?
~
# Possible Strategies
# Using @supports
A first approach I used was to use feature detection.
@supports(background: color(display-p3 1 1 1)) {
:root {
--hotterpink: color(display-p3 1 0 0.87);
}
}
:root {
--hotpink: var(--hotterpink, hotpink);
}
With this you can use var(--hotpink)
throughout your code. It will either resolve to the color(display-p3 1 0 0.87)
or to hotpink
, depending on support:
When the browser has support for color(display-p3 1 1 1)
, the custom property --hotterpink
will be declared and have a value of color(display-p3 1 0 0.87)
The extra Custom Property --hotpink
refers to that --hotterpink
. In browsers with display-p3
, that --hotpink
will take over the value of --hotterpink
. In browsers that don’t support display-p3
that --hotterpink
won’t exist, so it will fall back to a value of hotpink
.
See the Pen Move over hotpink … by Bramus (@bramus) on CodePen.
~
# Using @property
An alternative approach is to use @property
to register --hotpink
as a type of <color>
with an initial value of hotpink
, and then set its value to color(display-p3 1 0 0.87)
.
@property --hotpink {
syntax: '';
initial-value: hotpink;
inherits: true;
}
:root {
--hotpink: color(display-p3 1 0 0.87);
}
In browsers that don’t understand display-p3
, the override will not take place, thereby leaving --hotpink
to its initial-value. In browsers that do speak display-p3
the value will be set as normal.
See the Pen Move over hotpink, redux … by Bramus (@bramus) on CodePen.
💡 Note that this will only work in browsers that support @property
, which are Chromium-based browsers at the time of writing.
~
# Using a Space Toggle
UPDATE: reader Jane Ori commented with a third possible way: using a Space Toggle (also see this post by Lea Verou)
@property method is great because fallbacks are easy to use
@supports is great because it works in browsers without @property support
Best of both worlds, use a space toggle! :root space toggle, preferences & fallbacks defined in the same ruleset! 🎉https://t.co/6u5C06wMv5
— Jane Ori 💜 CSS Contortionist (@Jane0ri) October 13, 2022
The code looks like this:
@supports (background: color(display-p3 1 1 1)) {
:root {
--supports-display-p3: ;
}
}
body {
--hotterpink: var(--supports-display-p3) color(display-p3 1 0 0.87);
--hotpink: var(--hotterpink, hotpink);
background: var(--hotpink);
}
If no support for display-p3
is detected, --hotterpink
will be undefined and become the guaranteed invalid value initial
. In that case, --hotpink
will fall back to hotpink
See the Pen display-p3 Progressive Color Enhancement with CSS Space Toggles! by Jane Ori 💜 (@propjockey) on CodePen.
~
# Using @media
@media
If you thought of using @media
for this, you’re out of luck. The snippet below won’t work.
:root {
--hotpink: hotpink;
}
@media (color-gamut: p3) {
:root {
--hotpink: color(display-p3 1 0 0.87); /* ❌ Will not work as one might expect … */
}
}
Problem here is that @media (color-gamut: p3)
queries hardware support, not browser support. On a MacBook Pro in a browser that does not understand display-p3
the condition will evaluate to true, this because the MacBook’s display is capable of showing p3
colors.
~
# Screenshots
Here’s a screenshot of Chrome 108 with the Experimental Web Platform Features turned on, which enables (experimental) support for display-p3
:
And here’s a screenshot of stock Chrome 106 with no flags on, which does not support display-p3
:
~
# Automating things
# Automating things with PostCSS
If you want to automate things, you can use postcss
’s color function. It will automatically convert richer colors to sRGB compatible ones. Using its preserve
flag, you can keep both colors.
/* INPUT */
:root {
--a-color: color(srgb 0.64331 0.19245 0.16771);
}
/* OUTPUT */
:root {
--a-color: rgb(164,49,43);
}
@supports (color: color(srgb 0 0 0)) {
:root {
--a-color: color(srgb 0.64331 0.19245 0.16771);
}
}
/* INPUT */
.color {
color: color(display-p3 0.64331 0.19245 0.16771);
}
/* OUTPUT */
.color {
color: rgb(179,35,35);
color: color(display-p3 0.64331 0.19245 0.16771);
}
In that last example, browsers that don’t understand display-p3
will simply ignore that declaration, thereby falling back to the auto-converted RGB color.
# Automating things with a Sass mixin
Sparked by this post and a follow-up Twitter thread with Brandon McConnell, reader Josh Vickerson created a Sass mixin that lets you define multiple values in preference.
.background-hotter-pink {
@prefer('background', color(display-p3 1 0 0.87), hotpink);
}
The cool thing is how simple it actually is: it applies the given values in the reverse order that you defined them.
/**
* Generates a cascade of preferred values for a given CSS property
* @param $property: css property name you wish to provide values for
* @param $values: any number of values you'd like to use, in order of preference
*
*/
@mixin prefer($property, $values...) {
@for $i from length($values) to 0 {
#{$property}: nth($values, $i);
}
}
The output for the example above is this:
.background-hotter-pink {
background: hotpink;
background: color(display-p3 1 0 0.87);
}
Just like with the PostCSS plugin, browsers that don’t understand display-p3
will simply ignore the second declaration, thereby falling back to the hotpink
.
# Automating things with native CSS
Reader Brandon McConnell also created an issue within the CSS Working Group asking for a native prefer()
function that would work similarly to Josh’s function.
Turns out this already was on the radar of the Working Group, as Tab Atkins listed a similar issue two years ago.
The issue already underwent some discussion at the start of 2022, but it’s currently stalled. If you want to see this feature further evolve, feel free to chime in.
~
# Spread the word
To help spread the contents of this post, feel free to retweet the announcement tweet:
Upgrading colors to HD on the web
🏷 #color #css pic.twitter.com/oV16uRqndM
— Bram.us (@bramusblog) October 13, 2022
~
🔥 Like what you see? Want to stay in the loop? Here's how:
@property method is great because fallbacks are easy to use
@supports is great because it works in browsers without @property support
Best of both worlds, use a space toggle! One single @supports rule to set the global space toggle, fallbacks and preferred display color defined in the same ruleset! 🎉
https://codepen.io/propjockey/pen/jOxdeVY?editors=0100