Using Display-P3 colour

My personal site uses Ghost under the hood, which gives me a number of features to enrich my content. One of these features is accent colours. I can apply an accent colour to the whole site or to tags. You'll notice even on this page there's an accent colour affecting the links, icons and header.

<style>
    :root {
        --color-accent: {{ site.accent_color }};
    }
</style>
Example of how I use accent colours in my site

The above code is lifted straight from my site. I use custom properties throughout to  expose the accent colour to my CSS. The double curly brackets are for Nunjucks, the templating language I use with Eleventy to expose content from Ghost and build my site.

Display-P3

But of course that wasn't enough for me. I didn't want bright accent colours, I wanted really bright accent colours. Display-P3 is a colour gamut which extends further than other colour spaces such as sRGB. The WebKit blog goes into much finer detail on the subject.

Wide Gamut Color in CSS with Display-P3
Display-P3 color space includes vivid colors that aren’t available in sRGB.

At the time of writing the only way to access this extended colour space in CSS is by using the color() function and viewing the page in Safari. In addition, Ghost's accent colour feature is limited by hexadecimal colour codes and is therefore bound to the sRGB colour space. However, in typical fashion, I double down on the commitment to that hotter pink.

Converting hex to P3, the cheap way

Thankfully the route to that hotter pink is somewhat short. The steps I've worked out are based on the fact that Display-P3 written in a color() function is made up of red, green and blue channels. So in theory I could convert my hexadecimal colours into RGB values and then convert those into Display-P3 compatible values. Even though RGB channels range from 0 to 225 and Display-P3 0 to 1 it's not going to take much work to do the maths.

🟣
I called this method "the cheap way" because it kind of warps the actual value. If this was a brand colour it would be innacurate since I've shifted the colour more towards the extended P3 colour space. However for this use case I think it's ok as the colours are more of an asthetic thing.

I mentioned before that I use Eleventy to build my site. It's an extremely flexible tool, so flexible that I can build in my own filters in vanilla JavaScript to manipulate content. Here's the function I've put together to turn my hex colours into Display-P3 colour channels:

const hexToP3 = (string) => {
    const aRgbHex = string.replace("#", "").match(/.{1,2}/g);
    const aRgb = [
        (parseInt(aRgbHex[0], 16) / 255).toFixed(2),
        (parseInt(aRgbHex[1], 16) / 255).toFixed(2),
        (parseInt(aRgbHex[2], 16) / 255).toFixed(2),
    ];
    return `color(display-p3 ${aRgb.join(" ")})`;
};
Hex to P3 colour function

…and when I say "I've" I mean I based my function on the one shown here from Converting Colors:

Converting Colors - Convert HEX to RGB with Javascript
Converting Colors allows you to convert between color formats like HEX, RGB, CMYK and more. The current page shows the different conversions for Hex 3203C8.

I can then use this custom filter to convert the hexidecimal colours coming from Ghost into color(display-p3 …) colours.

<style>
    :root {
        --color-accent: {{ site.accent_color | hexToP3 }};
    }
</style>
Updated example code with the hex to P3 colour filter added

Compatibility

As mentioned earlier, the color() function only works in Safari, which means I need to provide some sort of fallback for the other browsers. I really hoped something like this would work:

<style>
    :root {
        --color-accent: {{ site.accent_color }};
        --color-accent: {{ site.accent_color | hexToP3 }};
    }
</style>
This does not work! Don't use this

I'd seen this method here on CSS Tricks but in both FireFox and Chrome the last property and value is applied but isn't understood as a colour, resulting in either no colour or black. I suspect this method did work at some point but has now changed, or is incompatible with custom properties.

Polyfilling

The other route is to make use of a CSS query, specifically the @supports query, to check if the browser supports the color() CSS colour function. The resulting CSS would look something like this:

:root {
    --color-accent: {{ site.accent_color }};
}

@supports (color: color(display-p3 1 1 1)) {
    :root {
    	--color-accent: {{ site.accent_color | hexToP3 }};
    }
}

This is fine, but not ideal. It's not so bad for when I'm using this accent colour for the whole page but I also use accent colours on particular elements.

<li style="--color-accent: {{ post.tags[0].accent_color }}">
	<svg class="icon  icon--{{ post.icon }}" aria-hidden="true">
		<use xlink:href="#{{ post.icon }}"></use>
	</svg>
	<div>
		<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
		<p>{{ post.excerpt }}</p>
	</div>
</li>

Here I'm setting an accent colour on a particular element, which overwrites the one on the page for just this element and all it's children. You can see the results of this on my homepage and related posts at the bottom of this page, there's a different accent colour for each post which gets applied to the icon and the heading link. This would get really messy if it was a whole <style> block with two blocks of CSS and an @supports.

Ideally I'd abstract that @supports synatax into my main CSS file and leave the pure colour values, both hex and P3, inline in my HTML. And I can do that thanks to CSS custom properties!

Let's change some stuff around. Firstly the inline colours, let's abstract them away from the main --color-accent property into their own specifically named ones.

--color-accent-hex: {{ accent_color }};
--color-accent-p3: {{ accent_color | hexToP3 }};
The accent_color variable here could be the site colour or the tag colour.

This, while a bit long winded, can be neatly used within an inline style attribute as well as inside a :root CSS block.

Next I need to designate which one of these custom properties is used for the actual --color-accent custom property depending on what the browser supports.

* {
    --color-accent: var(--color-accent-hex);
}

@supports (color: color(display-p3 1 1 1)) {
    * {
        --color-accent: var(--color-accent-p3);
    }
}

The clever part about this is the use of the wildcard selector. Using * means I'm selecting every single element on the page and telling it which custom property, hex or P3, to use as the actual --color-accent value. This catches not only the :root element but also all the elements which have --color-accent-hex and --color-accent-p3 set inline.

🟣
Just to note that despite all my examples showing custom properties being set in the template code, I do have them defined in my CSS in the main :root block as well incase all these values are suddenly not available.

All done! Now I have my really bright colours in the browsers that support Display-P3 and nice fallbacks for those that don't.

I hope you enjoyed this exploration into using Display-P3 on the web!

✌🏻