A short guide to switching themes with CSS

28 Jun 2024

User experience is paramount in web development, and a growing number of websites auto-adjust their frontend based on user preferences. For the purpose of demonstration, this guide will explore two methods for switching themes. First using the prefers-color-scheme media feature for setting the initial color scheme, and then enabling users to switch themes interactively.

With web browsers becoming increasingly smart, the simplest solution is to let the browser decide which theme to load based on the system settings. This can be achieved with the following CSS:

html {
    color: black;
    background-color: white;
}

@media (prefers-color-scheme: dark) {
    html {
        color: white;
        background-color: black;
    }
}

Note the black-on-white theme when the mode is light and its inversion, white-on-black, when the mode is dark. This method respects user preferences and requires no interactivity. For added functionality, however, we can also allow users to toggle between the two themes, so let’s see it in action by clicking on the icon below.

How it works

To understand how this works, let’s break it down into three components: HTML, CSS, and JavaScript, starting with an HTML button with an icon labeled theme-icon.

<!-- HTML -->
<button id="theme-toggle" onclick="toggleTheme()">
    <span id="theme-icon"></span>
</button>

Styling with CSS

Next, we define the styles for both light and dark modes using CSS variables. We also style the button dynamically to change its appearance based on the current theme. The trick here is to use the #theme-icon::before pseudo-element to change the icon conditionally.

<!-- CSS -->
<style>
html {
    background-color: #fff;
    color: #434343;
}

html[mode="dark"] {
    background-color: #151515;
    color: #e0e0e0;
}

html[mode="light"] {
    background-color: #fff;
    color: #434343;
}

#theme-icon::before {
    content: "🔆";
    display: inline-block;
}

html[mode="dark"] #theme-icon::before {
    content: "🌙";
}

#theme-toggle {
    background: none;
    border: none;
    cursor: pointer;
}
</style>

Adding JavaScript

Finally, we’ll use JavaScript to store the user’s preference in localStorage, ensuring the selected theme persists across page reloads.

// JavaScript
function toggleTheme() {
  const app = document.documentElement;
  const currentMode = app.getAttribute("mode");

  if (currentMode === "dark") {
    localStorage.setItem('mode', "light");
    app.setAttribute("mode", "light");
  } else {
    localStorage.setItem('mode', "dark");
    app.setAttribute("mode", "dark");
  }
}

// Apply the saved mode on page load if it exists
(function () {
  const app = document.documentElement;
  const savedMode = localStorage.getItem("mode");

  if (savedMode) {
    app.setAttribute("mode", savedMode);
  } else {
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    app.setAttribute("mode", prefersDark ? "dark" : "light");
  }
})();

The immediately invoked function ensures the correct theme is applied on page load by checking localStorage for the saved mode and setting the mode attribute on the <html> element. To improve accessibility, you may consider adding an aria-label for screen readers.

Conclusion

While there is no strict consensus on the best practice, allowing users to switch themes interactively can improve user engagement when implemented thoughfully. However, to keep our codebase small without compromising accessibility, I’d vote for the first approach, but let me know what you think.

Further reading