Dark theme switcher

Being a huge fan of “dark themes,” or as it is also called “dark mode,” I have implemented it across almost all the apps and websites I’ve built for myself — starting from this blog and ending with my latest project, It Changed.

I wasn’t satisfied with the solutions I found on the internet because they didn’t combine a selector and media queries. Why is this important? First of all, users can have predefined preferences. For example, on macOS, if you choose System Settings >> Appearance >> Dark, it will pass (prefers-color-scheme: dark) when the user visits a website. However, there may also be situations where the user wants to switch the theme to light for a specific website.

To provide the best UX, it must be a mix of solutions- which, as you’ll see, isn’t too difficult to implement.

So, let’s dive in.

Implementation

First, we need some kind of switch. I really enjoy using checkboxes for debugging purposes. Here’s a simple example:

<label type="button">
    <input type="checkbox" name="theme-switch" class="theme-switch"/>
    <span>switch</span>
</label>

Now let’s move to the JavaScript part:

const themeSwitches = document.querySelectorAll('.theme-switch');
if (themeSwitches.length > 0) {
    themeSwitches.forEach((themeSwitch, i) => {
        if (localStorage.getItem('dark-mode') === 'true') {
            themeSwitch.checked = true;
        } else {
            themeSwitch.checked = false;
        }

        themeSwitch.addEventListener('change', () => {
            const { checked } = themeSwitch;
            themeSwitches.forEach((el, n) => {
                if (n !== i) {
                    el.checked = checked;
                }
            });
            if (themeSwitch.checked) {
                document.documentElement.classList.add('dark');
                localStorage.setItem('dark-mode', true);
            } else {
                document.documentElement.classList.remove('dark');
                localStorage.setItem('dark-mode', false);
            }
        });
    });
}

This handles all the logic for the switcher and stores its state in localStorage. Since the selector is accessible only after the page is loaded, this code doesn’t need to run immediately. It’s a good practice to place it in your JavaScript file for deferred execution.

However, there’s a part of the code that does need to execute as early as possible. Here’s the snippet:

<script>
    if (localStorage.getItem('dark-mode') === 'true' || (!('dark-mode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.querySelector('html').classList.add('dark');
    } else {
        document.querySelector('html').classList.remove('dark');
    }
</script>

What this does:

  1. It checks if dark mode is set in localStorage;
  2. If localStorage doesn’t contain a dark mode setting, it checks whether the user prefers dark mode.

What this ensures:

  1. Users get dark mode if they’ve manually enabled it during previous visits.
  2. Users get dark mode if their system preferences indicate they prefer it.

Design

Since we implemented the switcher as a checkbox, it offers a wide range of design possibilities. It can function as a toggle, remain as a checkbox, or transform into a button- the options are virtually unlimited. Because of this flexibility, I won’t provide a single design solution here. Choose the one that best fits your use case.

Links

Here are some usefull links:

  1. I have published this code as a Composer package on Packagist.
  2. If you’d like to contribute, check out the project on GitHub.

That's all folks :)