How to implement a fool-proof dark mode

Published July 15, 2019
.* :☆゚

I think it’s fair to say dark mode is one of 2019’s biggest design patterns, especially amongst developers. Despite it’s popularity, dark mode implementation can be iffy, especially on purely static sites where dark mode is more likely to be found.


What is dark mode?

Dark mode simply means inverting light and dark colours on a website. Mac OS recently implemented dark mode in its settings, and the @media (prefers-color-scheme: dark) media query is slowly being updated in current browsers.

If you want a working example, click on the unicorn on this site to toggle the themes (to the left if you’re reading this on desktop, in the menu if you’re reading on mobile).


Adding dark mode to a website sounds trivial. In my opinion, it is not.

Sure, you can use CSS checkbox hacks if you want dark mode on a per page basis. If you’re using a JS based app you can probably more easily set the state across multiple pages. If you are using pure HTML and CSS like I am though, theme switching becomes a lot more finicky. And that’s not even mentioning the additional styles that comes with re-theme-ing a site, implemented in a such a way so that it doesn’t compromise the overall site design.

Many developers will load scripts in the head or footer. This is not necessarily a bad thing, but it can be easy to forget that loading in external assets takes a few extra seconds/milliseconds in bandwidth to load.

In web development, milliseconds is pretty significant especially if you’re using JavaScript to set and remember a user’s preference. If the JavaScript takes too long to load, the user’s preference may load in way later than your default styles, meaning you’ll get a flash of the opposite theme while the rest of the page is loading. This is especially noticeable when you’re navigating from one page to another.

Here is an example from my own website a few deploys back:


I’ve come across dark mode theme flashing many times visiting other people’s websites, some of whom are developers I greatly admire. It was even a bug on this site for the longest time, but I was too lazy to fix it until today because I really just threw it in here for fun and as a little easter egg.

To many people who don’t care to look into it too much, these minute changes between pages will be unnoticeable or at the very least passable.

To me however, I see all the little details, and seeing how often I notice this bug it makes me wonder why people even bother implementing a dark mode to begin with. Coupled with my slow internet connection and Netlify not caching assets by default, it usually becomes obvious to me when a website’s dark mode has not been implemented properly.

I call this phenomenon the flash of opposite colour-scheme.

FOOC for short.


So what’s a person gotta do to get that perfect dark mode?

There are many ways to implement dark mode, but to do it so that user’s preferences are remembered across the website, you’ll need to use JavaScript.

Using the localstorage property, we can ensure the implementation is fool-proof regardless of internet speed or page weight by doing two things:

  • Putting a conditional inline within the markup(in the head) to ensure it runs as soon as the page loads.
  • Checking the localstorage item and nothing else- not the body class, inline CSS blocks etc.

My current setup for this site is something like this:

1. Style the default theme of the site using CSS properties set on the :root.

2. Add some alternate properties using a different class.

    :root {
        --theme: #574d4a;
        --bronze: #a07c59;
        --darkbronze:#886547;
        --grey:#edecf7;

        .dark & {
            --theme: #d1d0dc;
            --darkbronze:#ca8057;
            --bronze: #9f9fc4;
            --grey:#4e4d63;
        }
    }

3. Add a dark mode button in the markup.

<button id="unicorn"><span class="screen-reader-text">Toggle dark mode</span></button>

4. Attach a click event to the button.

Using an external file is fine since this functionality is not dependent on how fast a page loads.

const unicorn = document.getElementById('unicorn'),
      root = document.getElementsByTagName( 'html' )[0];
unicorn.addEventListener('click', function () {
    //check if dark mode has been activated yet
    //if it has not, set up the dark mode!
    // at the same time, toggle the theme classes on the html tag
    //sidenote: I don't recommend applying custom classes to the body tag. If you're theme-ing a site you want the highest specificity possible

    if( localStorage.getItem('theme') !== 'dark' ){
        localStorage.setItem('theme', 'dark');
        root.classList.remove('light');
        root.classList.add('dark');
    } else {
        localStorage.setItem('theme', 'light');
        root.classList.remove('dark');
        root.classList.add('light');
    }
}, false);

5. Add an inline conditional before the closing head tag to check if a preference has already been set by the user.

The most important thing here is to make sure to check with the user’s localstorage and not JavaScript dependent CSS classes.

<script type='text/javascript'>
var root = document.getElementsByTagName('html')[0];
if (localStorage.getItem('theme') === 'dark') {
    root.classList.remove("light");
    root.classList.add("dark");
} else {
    root.classList.remove("dark");
    root.classList.add("light");
}
</script>

And that’s pretty much it.

The code is simple enough, but it’s how and where it runs that matters.

With this method you can then use the .dark class to style more complex things like scrollbars and placeholders in external stylesheets.

::-webkit-scrollbar {
	width: 15px;
	@extend %lightbg;

	.dark & {
		@extend %darkbg;
	}
}

The advantage of this technique is that it is by default designed to degrade gracefully. Since all the dark mode styles are dependent on the JavaScript-dependent .dark class, if JavaScript is not enabled in the browser for whatever reason, the site will simply use it’s default styles.