Supporting macOS Mojave’s Dark Mode on the web

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):

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!

Light and dark mode shown side by side


Thanks for reading! If you’re enjoying my writing, I’d love to send you infrequent notifications for new posts via my newsletter. You’ll receive the full text of each post, plus occasional bonus content.

You can also follow me on Twitter (@kevinchen) or subscribe via RSS.