Implementing dark mode in a handful of lines of CSS with CSS filters
I finally got round to implementing dark mode for this site (the cobbler’s children have no shoes and all that…)
Here’s all the CSS I had to add:
@media (prefers-color-scheme: dark) {
/* Invert all elements on the body while attempting to not alter the hue substantially. */
body {
filter: invert(100%) hue-rotate(180deg);
}
/* Workarounds and optical adjustments. */
/* Firefox workaround: Set the background colour for the html
element separately because, unlike other browsers, Firefox
doesn’t apply the filter to the root element’s background. */
html {
background-color: #111;
}
/* Do not invert media (revert the invert). */
img, video, iframe {
filter: invert(100%) hue-rotate(180deg);
}
/* Improve contrast on icons. */
.icon {
filter: invert(15%) hue-rotate(180deg);
}
/* Re-enable code block backgrounds. */
pre {
filter: invert(6%);
}
/* Improve contrast on list item markers. */
li::marker {
color: #666;
}
}
How it works
The real magic happens in the first rule:
body {
filter: invert(100%) hue-rotate(180deg);
}
I run an invert filter alongside a hue rotate filter on the body element. That inverts the colours on the page while attempting to keep the hues as similar as possible.
You might be wondering why I didn’t apply this rule to the root html
element. Thing is, originally, I did1. However, unlike other browsers, Firefox does not apply the invert filter to the background of the root element.
So, instead, I have to set the HTML element’s background manually, in the section on workarounds and optical adjustments:
html {
background-color: #111;
}
(Since the background colour of the page is #eee
, I set the dark mode background to its inverse, #111
, which matches what the invert filter does for the body.)
Next, I re-inverting media like images, videos, and iframes so they appear as originally intended:
img, video, iframe {
filter: invert(100%) hue-rotate(180deg);
}
(As you can see, I’m simply running the same rule again to revert the invert.)
Finally, the rest of the rules are little opticals tweaks, to improve contrast on a few elements like the icons in the header and the backgrounds of code block that don’t fare well with the inversion filter:
/* Improve contrast on icons. */
.icon {
filter: invert(15%) hue-rotate(180deg);
}
/* Re-enable code block backgrounds. */
pre {
filter: invert(6%);
}
/* Improve contrast on list item markers. */
li::marker {
color: #666;
}
If you’re looking to implement dark mode on your web site, you don’t use shadows/shades to denote hierarchy, and you don’t have a huge amount of time on your hands to craft a separate stylesheet, CSS filters are your friend.
Enjoy!
Like this? Fund us!
Small Technology Foundation is a tiny, independent not-for-profit.
We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.
-
Thanks to Kieran Barker, varx, Silmathoron, and others on the fediverse for pointing out the issue with Firefox and suggesting potential fixes. ↩︎