If it seems like my routine programming has been a bit thin—I believe I checked in a script, recently, where I only changed a constant that nobody but me will ever notice, just to have committed things—it’s because a fair amount of my time has gone to a project that I believe I’ll be announcing in the next week or…let’s say three, just in case.

Regardless, I spent a day or two on rigging up a dark mode for the site.

A man in darkness

It’s not a difficult idea, but it’s just complicated enough to warrant a tech tip, so that I can easily find the information the next time I need it.

What Are Dark Modes?

A dark mode, dark theme, night mode, or light-on-dark color scheme is a scheme for supporting people who (temporarily or permanently) would prefer to view content with darker colors in the background. There are various reasons for supporting such things, like protecting night vision or conserving power on devices.

They don’t always work well, especially in well-lit areas or for people with certain vision problems.

Designing a Dark Mode

Really, putting together a good dark mode is the same process of creating a good style for a website, except that you apply your darker colors to the background elements and lighter colors to the foreground elements. When I prototype dark layouts for this blog—no promises that I’ll ever actually release such a thing—for example, I generally use the blue color of the text as the background, use the golden color of the background for the text, and lighten the red headers.

If you have followed the sort of advice I gave in the post on color, you should be in good shape to do this. Specifically, your colors should be distinct and have enough contrast that you can change their use with almost no restrictions.

If you’d rather not put in the work, there are many people who have published dark color schemes that have become popular, such as Solarized and Dracula. I don’t particularly like them, personally, but plenty of people do.

Something that people don’t generally consider is that, if your dark mode has a lower contrast than your light mode, you might want to reconsider the fonts that you use. Treat low contrast as if you’re imposing a disability on your users. For example, for servers that I run at home, I switch the font on my RSS reader to OpenDyslexic when I change to the dark theme. I’m not dyslexic, but in being designed to make it easier for dyslexic people to read, it’s just generally easier to read.

You might also consider filtering images or making them partly transparent, so that a bright image isn’t too jarring.

Beyond that, you probably get the general idea.

Creating the Dark Styles

Putting a dark theme together is easier than it sounds, if you’re already familiar with CSS. It’s a bit ugly, though, because it involves duplicating styles for anything that has a color. Here’s a simple example.

body {
  background-color: white;
}
.dark-mode body {
  background-color: black;
}
#main-container {
  color: black;
}
.dark-mode #main-container {
  color: white;
}

This is the basic idea. Every object (or class of objects) on the page has two styles, one of which is the object/class that happens to be inside something that has the dark-mode class attached to it. So, when a top-level object (such as the body element) has dark-mode added to its class list, everything changes color.

Off-Ramp or Further Enhancement

If you want to bail out near here, handle everything in CSS, and not give the user the opportunity to choose your color scheme, you can use the prefers-color-scheme media type and just call it a day, you can rewrite the above like the following.

@media (prefers-color-scheme: light) {
  body {
    background-color: white;
  }
  #main-container {
    color: black;
  }
}
@media (prefers-color-scheme: dark) {
  body {
    background-color: black;
  }
  #main-container {
    color: white;
  }
}

My personal opinion, though, is that you probably shouldn’t provide a choice for a user, make that choice on the user’s behalf, and refuse to give the user a way to override the choice. So, you could stop here, but it’s worth continuing on.

Have Some Class

That last piece—adding dark-mode to an object’s class list—is the non-trivial part.

There are a few ways to handle this, of course. If you’re opposed to running JavaScript on your page, you can replace the .dark-mode class with an on-screen widget/control at the top of the page with a #dark-mode ID. In that case, you might have something like the following style, instead.

#dark-mode:checked ~ #main-container {
  background-color: black;
  color: white;
}

If you’re not familiar with it, the tilde character (~) says to take anything that answers to the ID #main-container, as long as it shares the same container as #dark-mode (when checked) and appears below it. And that works, as long as you don’t need to re-style anything above that widget. (I’m taking it on faith that anybody interested enough to read this far down can work out how to add the checkbox or change that style to a different kind of control. But I could be wrong about my audience, so don’t be shy if that’s not true.)

However, usually, you handle this with JavaScript.

document.body.classList.add('dark-mode');
// or
document.body.classList.remove('dark-mode');

That’s nicely straightforward.

Persistence

The foregoing takes care of the literal request to add a dark mode to a website. There’s a color scheme and a way to activate it. Done. 👍

However, nobody wants to make that kind of choice every time they visit a website. If the user wants dark mode to conserve power or to help preserve night vision, it would fail them greatly to present them with the normal style on every visit and then let them decide to change it.

Because of that, we want to store the choice.

Years ago, we would probably have stashed that choice in an HTTP cookie. That’s fine, and you can still do that, especially if you’re already setting cookies for things like authentication.

Modern browsers, however, support localStorage, which is a small database on the user’s browser just for your website. It’s cleaner to go there than figure out how long you want the cookie to survive, then pack up (and unpack) the cookie data correctly. In contrast, it looks something like this.

function setDarkness(isDark) {
  localStorage.setItem('should-site-be-dark', isDark ? 'dark-mode' : '');
  setDarkness(document.body);
}
function setDarkness(item) {
  const darkClass = localStorage.getItem('should-site-be-dark') || '';
  item.className = darkClass;
}
document.addEventListener('load', () => {
  setDarkness(document.body);
});

When the page loads, it checks the database for the current class to use (none or .dark-mode) and replaces the top-level body’s class with whatever it finds. Likewise, when the user specifically selects a style, it updates the class in the database and then repeats the check in the previous sentence.

You would probably want to put in a little more work than this, like setting the default state of the widget the user uses to change styles, but this is the core concept.

A Gem of a Twist

That’s all great in theory, except that my project uses Ruby on Rails. To increase rendering speed, Rails builds in Turbolinks or something like it. That speed comes from not rebuilding the entire page unless there’s no similarity between the old page and the new.

As you can see on the GitHub repository, Turbolinks is no longer under development…except maybe it is, because it’s not hard to find people recently complaining that the project’s development process is so opaque, and not all the documentation reflects what the code does, between sites. Those sound like something has been changing.

That’s all to say that my project can’t just set a CSS class on the body element when the page loads, because the page almost never loads.

Instead, I need more event handlers.

document.addEventListener('turbolinks:before-render', (event) => {
  setDarkness(event.data.newBody);
});
document.addEventListener('turbolinks:load', () => {
  setDarkness(document.body);
});

This runs the dark mode check whenever Turbolinks updates parts of the page with another page. That might be a bit much, but this task is lightweight enough that it probably doesn’t have a performance impact.

Bonus: Loading Spinners

As long as I’m dealing with Turbolinks and know the real custom events being used, I can use that to add a “loading” screen, in case a page takes a while to show up.

For that situation, we can perform the same sort of work—changing the CSS classes—for a box containing a “please be patient” message. We show it when the turbolinks:request-start event fires and hide it when the turbolinks:load event does. We’re already handling the latter in the previous section, so that’s one step ahead.

I’ll skip the other details of how to create and style the screen, though. If I dipped into every half-baked idea that I got through scanning the Turbolinks pages, we’d be here forever.

Go in Darkness

The above should just about do the job, as long as you connect all the pieces to the elements of your website. Would I go to this kind of effort on a regular basis? Probably not. For example, I chose the yellowish background of this blog to reduce eye strain, and people who adamantly dislike it can read the posts through many other channels.

For sites that might benefit from a dark mode, though, it’s only a couple of hours’ work…assuming that you know this ahead of time. So, this is the post that I wish I had on Thursday, before I started this project.

And it should be obvious that—except for the media query being limited to light/dark—you could also extend this restyling to any set of choices. I mean, you probably shouldn’t burden your user like this, but if you wanted a dark and light style for based on ever color, that’s entirely possible.


Credits: The header image is Dark by Transformer18, made available under the terms of the Creative Commons Attribution 2.0 Generic license. I don’t recommend messing with the color balance of the image. It’s far less dramatic when it’s just a pasty bald guy leaning against a door frame…