Why I removed my dark theme switcher

Ciprian on Tuesday, May 24, 2022 in Blog, JavaScript DOM

NEW! Learn JavaScript by example. Code snippets, how-to's and tutorials. Try now!

Over the course of several years, I have been implementing a dark/light theme switcher using JavaScript for both WordPress and static websites. The reason was being trendy. Everyone was doing it, big names, big websites, communities and so on. I do agree, it’s better to have a dark theme, especially when doing a lot of reading on your phone in dark places or in the evening.

Dark theme is one of the most requested features over the past few years. Both Apple and Google made a dark theme an essential part of UI. Dark theme’s reduced luminance provides safety in dark environments and can minimize eye strain.

UX Planet

Tech giants such as Apple, Facebook, Twitter, YouTube, and Reddit jumped on the trend and started offering users the option to toggle back and forth between dark mode and the original interface.

Superhuman

It’s just like reading books on your mobile device, you always go with the dark theme, as it’s easier on the eyes.

Shift Dashboard — Dark Mode by Sergey Zolotnikov

First, let’s see how I built it.

How to build a dark theme switcher

The first step is to place a physical switch somewhere on your page, in a highly visible page. I decided to go with the main navigation area:

Dark mode switch
Dark mode switch

The switch is a simple checkbox element, styled to look like a toggle switch:

<a class="toggle-container" href="#">
    <input type="checkbox" id="switch" name="theme"><label for="switch" aria-label="Dark Mode"></label>
</a>

Here is the required CSS:

/**
 * Light/dark theme switcher
 */
input[name="theme"] { /* styling for the <input> element */
    width: 0;
    height: 0;
    margin: 0;
    padding: 0;
    display: none;
}

.toggle-container label { /* styling for the <label> element */
    display: inline-block;
    cursor: pointer;
    width: 32px;
    height: 18px;
    background: #cccccc;
    color: #ffffff;
    border-radius: 48px;
    position: relative;
}

.toggle-container label:after { /* styling for the toggled <label> element */
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    background: #fff;
    border-radius: 100%;
    transition: 0.3s;
}

input[name="theme"]:checked + label { /* conditional check while toggling */
    background: #555555;
}

input[name="theme"]:checked + label:after {
    left: calc(100% - 2px);
    transform: translateX(-100%);
}

You see how simple it is. Now you have a fancy toggle button that does nothing. We need to add functionality using JavaScript and store the state dynamically.

The JavaScript code is a little longer than it needs to be, because I wanted to be able to add multiple switches on the same page, mainly for both the desktop navigation and the mobile one. On some of my implementations, a third element would appear on the page, in the user’s account. Not all of them are visible at the same time, but they are present in the source code.

document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('switch')) {
        let theme = localStorage.getItem('data-theme');

        const checkboxes = document.querySelectorAll('input[name="theme"]');
        const changeThemeToDark = () => {
            document.documentElement.setAttribute('data-theme', 'dark');
            localStorage.setItem('data-theme', 'dark');
        }

        const changeThemeToLight = () => {
            document.documentElement.setAttribute('data-theme', 'light');
            localStorage.setItem('data-theme', 'light');
        }

        if (theme === 'dark') {
            changeThemeToDark();

            [].forEach.call(document.querySelectorAll('input[name="theme"]'), checkboxEach => {
                checkboxEach.checked = true;
            });
        } else {
            changeThemeToLight();

            [].forEach.call(document.querySelectorAll('input[name="theme"]'), checkboxEach => {
                checkboxEach.checked = false;
            });

        }

        [].forEach.call(checkboxes, checkbox => {
            checkbox.addEventListener('change', () => {
                let theme = localStorage.getItem('data-theme');

                if (theme === 'dark') {
                    console.log('theme is dark');
                    changeThemeToLight();

                    [].forEach.call(document.querySelectorAll('input[name="theme"]'), checkboxEach => {
                        checkboxEach.checked = false;
                    });
                } else {
                    console.log('theme is light');
                    changeThemeToDark();

                    [].forEach.call(document.querySelectorAll('input[name="theme"]'), checkboxEach => {
                        checkboxEach.checked = true;
                    });
                }
            });
        });
    }
});

If you only need one switch per page, then get rid of (and refactor) all the document.querySelectorAll calls. Although, I think this way is more scalable and works in a set-it-and-forget-it way. It won’t break the next time a new developer adds more than one switch to the page.

Note: Don’t forget to remove the console.log() calls from the code after you tested it.

For storing the current state, I use localStorage.setItem, which stores the selected theme on a browser basis.

The last piece of the puzzle are the actual CSS styles. Nothing mysterious here, though, I just kept my original styles, and added the secondary theme ones (in my case, the light one). Something like the below (note, I make heavy use of CSS variables):

html[data-theme="light"] {
    --whiskey-accent: #5f27cd;
    --whiskey-accent: #6248ff;
    --whiskey-color: rgba(49, 52, 87, 0.8);
    --whiskey-background: #f7f7f8;
    --whiskey-nav-background: #ffffff;
    --whiskey-panel-background: #eaeaeb;
    --whiskey-radius: 2px;

    --whiskey-pastel-green: #4bdbb7;
}
html[data-theme="light"] h1,
html[data-theme="light"] h2,
html[data-theme="light"] h3 {
    font-weight: 700;
    letter-spacing: -0.011em;
}

I declared the colour variables at the top of my stylesheet, in the :root element. After the initial declaration, I simply change the colours for my light theme. Optionally, I make some headings bold for better visibility. Again optionally, I might have removed shadows for the dark theme, because they are not visible anyway.

Note that you don’t need more than that. It’s usually colours that change in a theme, so, if your theme has, say, 5 colours, you only require 5 CSS declarations. What would these colours be?

  • Headings
  • Body text
  • Additional/helper/small text (this is usually gray, and it fits both dark and light themes, so no change is required)
  • Buttons (optional, as they might need to stay the same colour, based on your branding guidelines)
  • Other text (any other text or content I can’t think of right now, based on your website design)

Further improvements and ideas

The JavaScript code needs no dependencies, and it is fast enough to allow for enhancements, such as recording how many users select a specific theme. An asynchronous request can be fired to an analytics solution of choice with the selected theme, and further marketing decisions can be made based on these statistics.

If you want to go overboard (and confuse your users) go for multiple themes: “hellish red” or “calm green”, to name a few 🙃

Why did I remove it?

Second, and this brings us to the actual title of the article, let’s see why I got rid of it from my personal website and some of my managed client websites.

As this is a JavaScript solution, the local storage needs to be checked on document load and this takes a few milliseconds, sometimes half to one second. This causes a dark-to-light (or light-to-dark, depending on your primary theme) flash, which is unprofessional. Moreover, Google doesn’t like it, and it will affect your Core Web Vitals.

As a quick note, in my implementation I have changed the size of some headings for the dark and light themes. So, switching the themes would result in a small page jump. Probably unnoticeable to users, but highly “annoying” for 🤖 Googlebot.

Conclusion

So, while the implementation is highly performant and concise, I have made my decision purely based on marketing and search engine optimization. My analytics solution is showing that even in 2022, my desktop pageviews are higher than mobile ones, so again, a light theme is more professional. Note that these are my personal websites I am talking about, and they have helped me land some nice roles during my career.

A personal website is 1000 times better than a LinkedIn CV.
(You can quote me on that and link to this page.)

Related posts

Leave a Reply