macOS Mojave adds a Dark Mode for native apps that makes you look approximately 78 percent cooler when using the computer. In Safari Technology Preview 68, it’s now available on webpages too! Here’s how I added support to this website.
Using the prefers-color-scheme
CSS media query
The release notes mention a new CSS media query for Dark Mode without saying how to use it. We can try to use a unit test from the WebKit repo as sample code instead.
In revision r237156, a test named
prefers-color-scheme.html looks promising. It shows that the new
prefer-color-scheme
media query can either be light
or dark
.
Let’s assume our CSS already looks good in light mode. We can use the media query to add overrides for some rules in Dark Mode:
/* The CSS rules we had before. */
body {
line-spacing: 1.2em;
color: black;
background: white;
}
@media (prefers-color-scheme: dark) {
/* Overrides for Dark Mode. */
body {
color: white;
background: black;
}
}
We can even put CSS variables in the media query. Unfortunately, it’s a pretty cutting edge feature: as of October 2018, only 91 percent of US web traffic supports it.1 That’s not enough for something as fundamental as setting the colors on a website.
Why don’t we use the media query for light mode too? Most browsers don’t know
about prefers-color-scheme
yet. If we had enclosed the light mode rules with
@media (prefers-color-scheme: light) { }
, none of the CSS rules would apply in
those browsers. Our styles would only show up in Safari!
Designing for Dark Mode
In the code above, we simply swapped the text and background colors. But that isn’t enough to make your site look great in Dark Mode. Apple’s WWDC talk Introducing Dark Mode is a fun, lightweight video that outlines their design philosophy and offers helpful app design tips. The same rules apply to websites.
Here are my main takeaways (but you should really watch the video because the presenter is more eloquent):
- Since text becomes white, links and other colored text should also become
lighter.
- Similarly, visual cues that normally become darker (like an active button) should become lighter in Dark Mode.
- Don’t mindlessly flip all the colors — not everything looks good inverted.
- Dark Mode is supposed to let the content shine, so don’t darken or invert things like images.
- Redraw icons to fill in areas that should be white.
Optional: Add more JavaScript
On this website, I already had a dark mode for the photo gallery. My site
generator would create the gallery page with <body id="dark">
to trigger the
dark CSS rules:
/* How Kevin's photo gallery works. */
body {
line-spacing: 1.2em;
color: black;
background: white;
}
body#dark {
/* Overrides for dark mode. */
color: black;
background: white;
}
I wanted to use the CSS rules I already had — duplicating all the rules from
body#dark
into a media query would be tedious and error-prone, especially
since the media query is an experimental feature.
Experts agree that any computer science problem can be solved by adding more
JavaScript. So I’ll listen to changes in the media query using JavaScript, then
modify the <body>
tag to match.
First, do the media query:
var mql = window.matchMedia('(prefers-color-scheme: dark)');
The mql.matches
flag will be true when Dark Mode is set. Add a callback to
mql
that runs when the media query changes. (We don’t have to animate the
transition. The system captures a screenshot of the entire desktop before the
transition and gracefully fades to the new appearance.)
function setDark(e) {
document.body.id = (e.matches ? "dark" : "");
}
mql.addListener(setDark);
So far, our code only runs when the Dark Mode setting changes. We also need to set the initial light/dark state when the page loads:
document.addEventListener("DOMContentLoaded", function() {
setDark(mql);
});
Here’s the whole thing:
var mql = window.matchMedia("(prefers-color-scheme: dark)");
function setDark(e) {
document.body.id = (e.matches ? "dark" : "");
}
mql.addListener(setDark);
document.addEventListener("DOMContentLoaded", function() {
setDark(mql);
});
That’s it!