Amit Dhamu

Software Engineer

Implementing Dark Mode

I recently implemented Dark Mode on this website. Let me talk you through the steps I took and what I wanted to achieve.

  1. Check the user's device setting for Dark Mode and apply custom styling if enabled.
  2. Have a toggle switch that allowed users to override this and switch to either dark or light mode.
  3. If they have overidden the device setting, store this preference in localStorage.

Check if Dark Mode is Enabled

I started off creating a new _dark-mode.scss file and importing it into my main stylesheet.

@media (prefers-color-scheme: dark) {
    body {
        // add dark mode styles
    }
}

Using the above, I went about going through each page on my site and setting styles that were applicable only to dark mode users. This was all pretty simple and mainly involved overriding existing styles.

Create a toggle switch

The first thing I needed to do was replace what I had above. I needed to detect dark mode using Javascript and then apply a custom CSS class to the <body> tag.

I created a helper function.

export const isPrefersDarkMode = () => {
    if (typeof window !== 'undefined') {
        return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
};

The typeof if statement above is best practice and ensures that Gatsby builds in production.

Secondly, I replaced my existing CSS to the below.

body.dark-mode {
    // add dark mode styles
}

At this point I'm ready to implement my Dark Mode Toggle component.

I won't go through the styling aspect of the switch as you can find many toggle/on-off switch styles on Codepen etc. I will show you how I got this functional though. The switch is based on a styled checkbox so all we need to be concerned about is the onChange event.

I'm using Gatsby and React so my component kind of looks like this.

import React from 'react';

// This is the helper function we created above
import {isPrefersDarkMode} from '@global/helpers';

const DarkModeToggle = () => {
    const [isDarkMode, setIsDarkMode] = React.useState(isPrefersDarkMode());

    return (
        <div className="dark-mode-toggle">
            <input
                type="checkbox"
                checked={isDarkMode}
                onChange={
                    () => {
                        setIsDarkMode(!isDarkMode);
                        document.body.classList.toggle('dark-mode', !isDarkMode);
                    }
                }
            />
        </div>
    );
}

export default DarkModeToggle;

Quite a simple component. It does a few things.

  • Uses local state for setting dark mode
  • Defaults to the user's system setting
  • Clicking the checkbox sets dark mode to the opposite of what is set in local state (toggling it)
  • Toggles the dark-mode CSS classname to the <body> tag

You should now be at a point where you have a functional toggle switch that switches dark mode on and off.

Storing the result of a toggle to Local Storage

Luckily, we don't have to add much to accomplish this.

In our helper function, we want to check if there's a property in localStorage called prefersDarkMode and return that preference or default to the user's system setting.

 export const isPrefersDarkMode = () => {
     if (typeof window !== 'undefined') {
+        if (localStorage.getItem('prefersDarkMode')) {
+            return localStorage.getItem('prefersDarkMode') === 'true';
+        }
         return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
     }
 };

Secondly, we want to store the preference in the onChange event of our toggle checkbox.

 onChange={
     () => {
         setIsDarkMode(!isDarkMode);
         document.body.classList.toggle('dark-mode', !isDarkMode);
+        window.localStorage.setItem('prefersDarkMode', !isDarkMode);
     }
 }

And that's it!

Add the Dark Mode class as early as possible on load

We want the dark mode class to be added as early as possible in the rendering cycle. This switch could be quite nested in the structure of your application. For this reason, I have added a useEffect hook in my entry-level component that leverages the isPrefersDarkMode helper function we created above.

import React from 'react';
import {isPrefersDarkMode} from '@global/helpers';

const App = () => {
    React.useEffect(() => {
        document.body.classList.toggle('dark-mode', isPrefersDarkMode());
    }, []);
}

export default App;

Final Thoughts

It's worth noting that items set in localStorage don't have any expiry like cookies. If you want to implement something that expires, you can switch out this implementation with cookies instead. Alternatively, you could use sessionStorage but that only lasts as long the tab or window is open.

Personally, I felt that localStorage was the best solution as the preference is remembered indefinitely unless the user decides to change it.


 dark mode / css / javascript / toggle / switch

Aliased Imports in Webpack

Show Comments

Made with by Amit Dhamu.
© MMXX. All rights reserved.