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>
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.

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 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(" ")})`;
};
…and when I say "I've" I mean I based my function on the one shown here from Converting Colors:

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>
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>
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 }};
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.
: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!
✌🏻