Adding a little dark magic to the web

A writeup of how I went about implementing a dark theme to support Mojave’s Dark Mode appearance in the marketing website for an app called Quids.

Dark Mode on the web

While existing browsers don’t have the ability to leverage it, Apple’s Safari team has made inroads to supporting the new ‘Dark Mode’ that shipped with macOS Mojave, and as of Safari Technology Preview 68 web developers can implement alternative styles via a newprefers-color-scheme media query.

@media screen and (prefers-color-scheme: dark) { }

While it may still be some time before we see this ship to regular Safari (and perhaps even further out before it makes it out of the draft spec and into other browsers) we can start designing and building for it today. Using the media query we can check for the user’s appearance preference and serve up alternative text colors and background fills with very little work. A little more effort and we can tailor images and graphics and serve those up too. With some more legwork we can even have all this work in currently shipping browsers too.


Quids for Mac

Quids is a Mac app myself and Red Davis recently launched (it’s a cryptocurrency manager for the Mac - more on that in another post).

I designed the UI with light and dark themes in mind and it ships with both. When it came to producing the marketing materials I wanted to make sure both appearances could get some airtime on the website. So with that in mind, here’s how I went about it.

1. Starting things off in Sketch

Sketch is my tool of choice so the first steps were beginning the designs as fairly high fidelity comps. I’m actually not a Dark Mode user myself so most of the initial design was the light theme. Rather than context switching between two designs I found it easier to get a complete first pass of one theme in before duplicating it, reversing the color schemes and then making additional edits to both at the end to tweak and polish.

Using Symbols where possible obviously makes keeping these two mockups in sync a little easier, and now that Sketch has override support for text styles and appearance styles altering the Symbols where required for one theme or the other. By using Symbols you can also reduce the number of duplicated assets for each theme.

Ideally we want to change as few elements as possible between themes, and only duplicate and diverge where absolutely neccessary. Switching out color palettes for text and backgrounds is trivial CSS thanks to prefer-color-scheme but there’s a little more involved when we switch out images. Simple glyphs, logos and other flat assets can also be styled with CSS (more on that in a bit), but complex graphics are likely to less theme-able and will need switching out entirely.

2. Image formats and exporting assets

Quids uses fairly rich graphics alongside some simpler glyphs and logos. The glyphs, such as the navigation icons, download buttons, and logos can all be exported as SVGs — to keep these as customisable as possible it’s worth flattening the shapes and points down as much as possible to keep filesize small and the DOM structure easy to target with CSS.

With the graphics for the features, the MacBook and the backdrop pattern, among others, I’m exporting at @1x @2x and again for the dark theme counterparts with an added -dark suffix. Naming and dimensions are kept identical so the swap will be as simple as possible when we get to the build.

3. The build

Most of my own projects are static builds — regular old HTML, some fairly uncomplicated handwritten SASS and some snippets of JS (usually some basic jQuery). The Quids website is no different and everything we need to do to support a switchable dark theme is supported with this particular stack without any additional dependencies.

While we can support Dark Mode natively in the Safari Technology Preview with our CSS media query, the images require a little more consideration. In the case of our SVG assets, we can style those using the same media query in the same fashion, as long as we embed the SVG in the HTML document. The bitmap images we’ll need to swap out with JS which we’ll come back to.

4. Adding some SASS

The bulk of supporting Dark Mode natively is handled by nesting alternative properties in our existing rules. We’re using it a lot so we’ll create a mixin for it to simplify things. This will work for our fonts, backgrounds and for styling our SVG assets —

@mixin dark-mode {
@media (prefers-color-scheme: dark) { @content; }
}
body {
color: $dark;
background: $light;
  @include dark-mode {
color: $light;
background: $dark;
}
  svg path {
fill: $dark;
    @include dark-mode {
fill: $light;
}
}
}

5. Some light scripting

As mentioned before, we’ll need some JS to swap our bitmap images out. We can do that by listening for a matchMedia() event and triggering our swap when it matches our prefers-color-scheme: dark media query. Whenever the appearance preference in macOS System Preferences is switched we can get that event and respond to it —

function toggleDarkMode(isDark) {
if (isDark.matches) {
makeImagesDark();
} else {
makeImagesLight();
}
}
var isDark = window.matchMedia('screen and (prefers-color-scheme: dark)');
toggleDarkMode(isDark);
isDark.addListener(toggleDarkMode);

The two functions makeImagesDark and makeImagesLight contain almost identical logic — they take the src of our image on the page and substitute it for the appropriate data-src attribute (depending on the theme and depending on the resolution we need, 1x or 2x).

<img src="assets/images/feature.png"
data-src-light="assets/images/feature.png"
data-src-dark="assets/images/feature-dark.png"
srcset="assets/images/feature@2x.png 2x"
data-srcset-light="assets/images/feature@2x.png 2x"
data-srcset-dark="assets/images/feature-dark@2x.png 2x" />
function makeImagesDark() {
$('img').each(function() {
$(this).attr('src', $(this).attr('data-dark-src'));
$(this).attr('srcset', $(this).attr('data-dark-srcset'));
});
}
function makeImagesLight() {
$('img').each(function() {
$(this).attr('src', $(this).attr('data-light-src'));
$(this).attr('srcset', $(this).attr('data-light-srcset'));
});
}

We now have a fully functioning dark theme that will be automatically be applied when users of Safari Technology Preview on Mojave set their appearance preference to Dark Mode. When they switch the preference the webpage will even crossfade between themes the same way native UI does.

6. Extra credit

There are a couple more features I wanted to add to the Dark Mode functionality for the Quids website that I’ll also cover here, namely —

  1. Supporting the theme in current browsers
  2. Letting the user switch between themes
  3. Remembering the theme between page loads

There is a section on the website where I display screenshots from the Mac app. You’ll notice on the live website that those are also themed according to the mode and there is also a nearby link that manually toggles between themes.

This toggle switch serves two purposes — allowing users to switch themes in browsers that don’t natively support prefers-color-scheme and also allowing users in the native Dark Mode to view the screenshots in Light Mode (and vice versus). In the case of those native Dark Mode users, we can just update the screenshots, and preserve the theme otherwise.

To manually switch between themes all we need to do is toggle a .dark-mode class on the body element and find a way to take our nested media query CSS and apply it directly to the child elements when namespaced under the toggled body class. We’ll apply this class when the switch link is clicked, and we’ll remember the setting using the browser’s localStorage so we can reapply it when navigating between pages and across sessions.

$('#lightswitch').on('click', function() {
if ($('body').hasClass('darkmode')) {
$('body').removeClass('darkmode');
makeImagesLight();
localStorage.removeItem("prefers-color-scheme", "dark");
return false;
} else {
$('body').addClass('darkmode');
makeImagesDark();
localStorage.setItem("prefers-color-scheme", "dark");
return false;
}
});
if (localStorage.getItem("prefers-color-scheme") == "dark") {
$('body').addClass('darkmode');
makeImagesDark();
}

Now that our class is applied to the body element in the correct scenarios, our images on the page are updated and we can store the preference locally. Lastly we need to update our SASS mixins so that we can apply our dark theme based on the presence of the body class and not just a media query —

@mixin dark-mode($rule) {
#{$rule} {
@content;
}
@media (prefers-color-scheme: dark) { @content; }
}
body {
color: $dark;
background: $light;
  @include dark-mode('&.dark-mode') {
color: $light;
background: $dark;
}
  svg path {
fill: $dark;
    @include dark-mode('.dark-mode &') {
fill: $light;
}
}
}

By passing the rule property to our mixin we can now return not only the media query, but a brand new rule alongside it that contains the same CSS properties. So if we take this following example based on the above here —

svg path {
fill: $dark;
  @include dark-mode('.dark-mode &') {
fill: $light;
}
}

.. once processed we end up with this compiled CSS —

svg path { fill: #333 }
.dark-mode svg path { fill: #fff }
@media screen and (prefers-color-scheme: dark) {
svg path { fill: #fff }
}

And now we have our rule for Light Mode, our rule for the manually triggered Dark Mode, and our media query rule for natively supported Dark Mode. There’s some finishing touches added to our final production version such as the addition of a key binding to t to switch modes, but otherwise what you see in this article is pretty much the entire implementation of Dark Mode for the Quids website!


Thanks for reading along! Leave a comment or send me a message on Twitter if I can shine some light on anything in this article.