Supporting dark mode in web content
Read in 7 minutes
Welcome to 2019, the year that the technology went dark mode. Both Android 10 and iOS 13 were released last month, and the most acclaimed feature was dark mode. Native apps can now implement specific themes for each mode (i.e. dark, light), but can you do it on the web? This article outlines how you can take advantage of this new trend and make everyone happy.
Are there any benefits on using dark mode?
When you read about dark mode, you often see “easy on the eyes” or “energy efficiency” being mentioned. Truth is, to be energy efficient you need OLED/AMOLED screens which are only available on newer high-end devices.
- iPhone X or newer.
- Samsung Galaxy S10 or newer.
- Google Pixel 3 or newer.
- Huawei P30huawei-p30 or newer.
If you’re using LCD or another type of screen, changing colors won’t do much for your battery life. Most people won’t benefit from the energy efficiency aspect of dark modes at all.
But what about health? It certainly is better, right? Well… unfortunately, we still don’t have enough scientific data to assert that dark mode is better. What we know for sure is that the time we spend looking at screens is the main factor to eye strain. The less, the better.
Some doctors also mention that a higher contrast (either way) is more important than using either mode. And some doctors mention that dark mode can be more damaging for people with astigmatism.
I personally don’t like dark mode at all when using dark background with light text (i.e. black background with white text); I have that annoying feeling of burnt image that takes a minute or so to disappear. And the opposite is also true for some people. And without serious researches about this subject we can only talk by experience.
Now, I think the biggest advantage of dark mode is when you’re using your phone in a low light room, but you would be better off not using your phone right before going to sleep anyway, right?
Browse support
How well supported is dark mode on today’s browsers? To detect whether a browser is running on a dark mode device or not, you’ll need support for (prefers-color-scheme: dark)
media query, available on the following devices.
- macOS Safari 13 or newer.
- iOS Safari 13.2 or newer.
- Android Browser 76 or newer.
- Chrome for Android 78 or newer.
- Firefox for Android 68 or newer.
- Opera 62 or newer.
- Chrome 76 or newer.
- Firefox 67 or newer.
- Microsoft Edge 76 or newer.
Yeah, I know… this is stupid versioning in action and I shouldn’t have added them anyway. What you need to know is that Safari 13 is very, very new. And so is the support for this media query on every other browser. From the business perspective, it doesn’t make sense to spend your designer’s time on this task, but you’re going to do it anyway, aren’t you? In this case, this is how you can define the CSS.
body {
color: #333;
background: #fff;
}
@media screen and (prefers-color-scheme: dark) {
body {
background: #2d3239;
color: #75715e;
}
h1 {
color: #e9d970;
}
}
You can easily alternate between light and dark mode using Safari’s Web Inspector. I couldn’t find such option on Chrome yet, but we’ll have something similar sooner or later. This example can be found here.
I know what you’re thinking… the CSS can easily get out of hand. And you’re right! Unless you use variables. More specifically, CSS variables.
Creating manageable themes with CSS variables
The trick to organize your CSS is to use variables. To define a variable, you can use the --variable-name: value
format.
:root {
--page-background: #fff;
}
body {
background: var(--page-background);
}
That simple. Now, how can we define both of our themes using variables? First, declare your light theme without any media query, like the following:
:root {
--page-background: #fff;
--page-title: #333;
--page-text: #333;
}
body {
background: var(--page-background);
color: var(--page-text);
}
h1 {
color: var(--page-title);
}
Now, adding the dark theme is as simple as wrapping the sample properties in a media query. The whole CSS code looks like this:
:root {
--page-background: #fff;
--page-title: #333;
--page-text: #333;
}
@media screen and (prefers-color-scheme: dark) {
:root {
--page-background: #2d3239;
--page-title: #e9d970;
--page-text: #75715e;
}
}
body {
background: var(--page-background);
color: var(--page-text);
}
h1 {
color: var(--page-title);
}
You can see this example here. You can define any properties to tweak your theme including (but not limited to) background images, borders, shadows, and filters.
Dark mode images and videos
It’s not uncommon to have bright images/videos all over your page. One nice trick is using filters to reduce the brightness of <img>
and <video>
. You can try a combination of opacity
and grayscale
filters. Add --image-grayscale
and --image-opacity
variables and tweak it as you wish:
:root {
--page-background: #fff;
--page-title: #333;
--page-text: #333;
--image-grayscale: 0;
--image-opacity: 100%;
}
@media screen and (prefers-color-scheme: dark) {
:root {
--page-background: #2d3239;
--page-title: #e9d970;
--page-text: #75715e;
--image-grayscale: 50%;
--image-opacity: 60%;
}
}
img,
video {
filter: grayscale(var(--image-grayscale)) opacity(var(--image-opacity));
}
You can see this example here. This trick won’t work everywhere and may requiring wrapping images in a container. Eventually, you’ll need a different image. That’s where <picture>
comes in.
The <picture>
element supports media query matchers. So, in case you want to specify a different logo for dark mode, you can use a different <source>
. If there are no suitable matches or if the browser doesn’t support the <picture>
element, then the default src
attribute is selected.
Let’s say that instead of rendering that same image using filters, you wanted to render a totally different darker image.
<picture>
<source srcset="beach.jpg" media="(prefers-color-scheme: dark)" />
<img src="pool.jpg" />
</picture>
As for the CSS, you can remove everything related to filters. I’ll leave this as an exercise for you. The end result can be seen here.
In some cases, you may use an embedded embedded SVG and change the colors. It works great for things like flat icons, logos, and that sort of thing, but sometimes you just have to render a different image. Let’s add a logo that can adapt to dark mode.
It’s important to know is that, to style <svg>
elements you have to actual render it on your HTML markup. Referencing it through an <img>
tag won’t allow any styles on the rendering SVG. With that said, the SVG we’re using looks like this:
<svg
id="logo"
width="250px"
height="55px"
viewBox="0 0 250 55"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo">
<path d="..." id="background" fill="#0091FF"></path>
<path d="..." id="letter" fill="#FFD700" fill-rule="nonzero"></path>
<path d="..." id="words" fill="#45638B" fill-rule="nonzero"></path>
</g>
</g>
</svg>
The colors applied to this SVG are the light mode colors, and by doing that we’re only required to style the dark mode. This is the updated CSS with the SVG styling changes.
@media screen and (prefers-color-scheme: dark) {
:root {
--page-background: #2d3239;
--page-title: #e9d970;
--page-text: #75715e;
--logo-background: #4d5866;
--logo-words: #fff;
}
#logo--words {
fill: var(--logo-words);
}
#logo--background {
fill: var(--logo-background);
}
}
You can check this example here.
Alternatively, you could have used currentColor
as the value of fill
and stroke
properties. This way, you can change all referenced colors by either setting the SVG’s color
property or the inherited color. This approach works extremely well with line icons.
The example above generates a new color every time the button is clicked and sets the <body>
element’s color
with document.body.style.color = newColor
.
Dark mode JavaScript
You may need to do operations that require detecting dark mode as well, like rendering charts. For that you’ll need to use window.matchMedia
. The detection is fairly simple.
function isDarkMode() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}
function renderCanvas() {
const theme = isDarkMode()
? { background: "#4d5866", border: "#7091ba" }
: { background: "#ffffff", border: "#000" };
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const x = 15;
const y = 15;
const width = canvas.width - x * 2;
const height = canvas.height - y * 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 5;
ctx.strokeStyle = theme.border;
ctx.strokeRect(x, y, width, height);
ctx.fillStyle = theme.background;
ctx.fillRect(x, y, width, height);
}
renderCanvas();
As you can see, all you have to do is conditionally define your theme once your media matcher detects the dark mode. You can see this example here.
Now, if we switch from one mode to the other (you may also have configured your computer to automatically do it for you), you would see the wrong colors being rendered. To re-render the canvas we can use MediaQueryList.addListener
to respond to the change.
const darkModeMatcher =
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
function isDarkMode() {
return darkModeMatcher && darkModeMatcher.matches;
}
function onDarkModeChange(callback) {
if (!darkModeMatcher) {
return;
}
darkModeMatcher.addListener(({ matches }) => callback(matches));
}
function renderCanvas(useDarkTheme) {
const theme = useDarkTheme
? { background: "#4d5866", border: "#7091ba" }
: { background: "#ffffff", border: "#000" };
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const x = 15;
const y = 15;
const width = canvas.width - x * 2;
const height = canvas.height - y * 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 5;
ctx.strokeStyle = theme.border;
ctx.strokeRect(x, y, width, height);
ctx.fillStyle = theme.background;
ctx.fillRect(x, y, width, height);
}
renderCanvas(isDarkMode());
onDarkModeChange(renderCanvas);
This will make sure the function renderCanvas
is called whenever the mode changes. You can see this example here.
And that pretty ends what you need to know about the technical aspects of supporting dark mode in the web.
Wrapping up
Dark mode is the latest trend in tech, no doubt about it. And even without having any serious research backing its so-called “dark mode benefits”, you may consider doing it for the sake of your customers’ happiness. The technical aspects are quite simple, but don’t fool yourself: creating dark themes is a very challenging process, specially when it comes to art direction for all assets (including images and video).
Another thing to consider is whether you should automatically switch to dark mode or use a configurable setting on your site. The latter is very simple to implement by setting a class on an element (e.g. <html data-theme="dark-mode">
), but you’d have to manually execute scripts and change images/videos in case you switched from one mode to the other. Either that, or not bothering at all, which would be (probably) fine in most cases.