How to implement a fool-proof dark mode

Last modified July 15, 2019
.* :☆゚

I think it’s fair to say dark mode is one of 2019’s biggest design patterns, especially amongst developers. Here’s a fool proof way to implement dark mode on your website so that it saves a user’s preference and ensures no flash of inverting colours on page load.


What is dark mode?

Dark mode simply means inverting light and dark colours on a website.

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

Implementing dark mode using pure CSS media queries is possible (using @media (prefers-color-scheme: dark) ), but I personally prefer allowing the user to manually toggle it on/off instead of relying on their browser preferences. The following article will focus on a JavaScript based version which will save the user’s preference as well as allow users to toggle between light and dark themes.


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

Not only do you have to literally re-theme your entire site (while ensuring same or better accessibility and contrast), you have to be aware of how and when your dark mode script loads in the first place.

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.

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. My method also makes use of CSS variables because it makes re-themeing a site much simpler. If you need to support IE 11 and below, be aware CSS variables are not supported, so you will have to use sass variables or a polyfill instead.

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 ASAP.

The method

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 {
        --text-color: #574d4a;
        --text-accent: #a07c59;
        --muted:#996547;
        --grey:#edecf7;

        .dark & {
            --text-color: #d1d0dc;
            --text-accent:#ca8057;
            --muted: #9f9fc4;
            --grey:#4e4d63;
        }
    }

3. Add a dark mode toggle in the markup.

A button:

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

or checkbox:

<label class="switch">
    <input type="checkbox" id="theme-toggle">
    <span class="toggle"></span>
</label>

It doesn’treally matter what kind of toggle you use in my opinion - either works just about the same. This site uses a button because I made the toggle an image, however I used the checkbox method on this site.

If you want a fancy sliding checkbox toggle, you may use the CSS below and adjust it to your own needs:

//dark mode toggle
.switch {
    display: inline-block;
    height:14px;
    position: absolute;
    top: -3px;
    right: 0;
    width: 26px;
    border-radius: 20px;
    margin-left: 30px;
    z-index: 101;
    @include media("<=menu") {
        top: 0px;
    }
    input {
        display: none;
    }

    .toggle {
        background-color: $brand-primary;
        bottom: 0;
        cursor: pointer;
        left: 0;
        position: absolute;
        right: 0;
        top: 0;
        transition: .4s;
        border-radius: 20px;

        &:before {
            background-color: $brand-secondary;
            bottom: 1px;
            content: "";
            height: 12px;
            left: 1px;
            position: absolute;
            transition: .4s;
            width: 12px;
            border-radius: 20px;
        }
    }

    input:checked + .slider {
        background-color: $brand-primary;
    }

    input:checked + .slider:before {
        transform: translateX(12px);
    }
}

4. Attach a click event to the toggle.

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
    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.

<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;
	background: white;

	.dark & {
		background: black;
	}
}

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.