Upgrading colors to HD on the web

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)

Screenshot of Safari’s Color Picker showing 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)

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

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:

Screenshot of Chrome 108, which has support for display-p3

And here’s a screenshot of stock Chrome 106 with no flags on, which does not support display-p3:

Screenshot of Chrome 106, which has no support for 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:

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

2 Comments

Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.