DEV Community

Kasper
Kasper

Posted on • Edited on

Dark Mode Switch Respecting System Theme

If you've ever wanted to have a simple "dark mode" toggle on your website, it can be a bit tricky because you should support three states: Dark, Light, and System. Additionally, there's keeping track of when the system theme changes (some people have auto dark mode at night).

What we can do is follow the system theme by default. If the user toggles the theme on our website, we'll set a theme item in localStorage to override the system theme. Then, if the user toggles the theme to match the system theme, we remove the override. The result is that the user can get the behavior they want just by switching the theme to what they want!

Here's a codepen with it in action:

You can also see my Svelte implementation of this on the Date Picker Svelte website (app.html, layout.svelte).

I've chosen dark mode as the default theme just in case there's a "Flash of incorrect theme", or FOIT. If the default theme was light, this flash would be bright, which is particularly distracting for dark mode users. FOIT is a risk because we need to load our theme in JavaScript, but it should rarely happen.

The <head>

<!-- Default theme, for when JS is disabled -->
<html lang="en" data-theme="dark">
  <head>
    <script>
      function detectTheme() {
        var themeOverride = localStorage.getItem('theme')
        if (themeOverride == 'dark' || themeOverride === 'light') {
          // Override the system theme
          return themeOverride
        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
          // Use the system theme
          return 'light'
        } else {
          // Default theme
          return 'dark'
        }
      }
      document.documentElement.setAttribute('data-theme', detectTheme())
    </script>
    <!-- other head tags -->
  </head>
  <!-- body -->
</html>
Enter fullscreen mode Exit fullscreen mode

First of all, we have the data-theme="dark" attribute in our <html> element. That makes sure our theme is still specified if JavaScript is disabled.

Inside <head>, there's our script. We want it to run as early as possible, so it's the first thing in <head>. The script simply sets the <html> data-theme attribute with whatever theme it detects. It first checks the theme item in localStorage. If there's no theme override, it uses the system theme or the default dark theme.

Updating the theme

Now for the JS that takes care of updating the theme:

// Get the systemTheme using window.matchMedia
const prefersDarkMQ = matchMedia('(prefers-color-scheme: dark)')
let systemTheme = prefersDarkMQ.matches ? 'dark' : 'light'
prefersDarkMQ.onchange = (e) => {
  // Keep the systemTheme variable up to date
  systemTheme = e.matches ? 'dark' : 'light'
  // Update the theme, as long as there's no theme override
  if (localStorage.getItem('theme') === null) {
    setTheme(systemTheme)
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we get the system theme in our systemTheme variable. When the system theme changes, we update our systemTheme variable and, unless we have a theme set override set in localStorage, we update the theme as well.

let theme = document.documentElement.getAttribute('data-theme') || 'dark'
Enter fullscreen mode Exit fullscreen mode

Next, we make a variable for the theme, using the value from the data-theme attribute which was set when the page loaded.

function setTheme(newTheme) {
  document.documentElement.setAttribute('data-theme', newTheme)
  theme = newTheme
  if (newTheme === systemTheme) {
    // Remove override if the user sets the theme to match the system theme
    localStorage.removeItem('theme')
  } else {
    localStorage.setItem('theme', newTheme)
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we define a function for updating the theme. It first updates the data-theme attribute and the theme variable. If the theme is updated to match the system theme, we remove our theme override from localStorage. If it's updated to something else, we set the override.

Now, for your toggle button, you just need a click handler like this:

function toggleTheme() {
  if (theme === 'dark') {
    setTheme('light')
  } else {
    setTheme('dark')
  }
}
Enter fullscreen mode Exit fullscreen mode

For your CSS you simply select the html[data-theme] attribute:

html[data-theme="dark"] {
  --bg: black;
}
html[data-theme="light"] {
  --bg: white;
}
html {
  background-color: var(--bg);
}
Enter fullscreen mode Exit fullscreen mode

Behavior

  • What if the website is overridden to dark, and I want to change it to follow the system when the system theme is dark? Toggle the theme to match the system once you notice it's not.
  • What if the website is light, and I want to change it to be overridden to dark when the system is already dark? Toggle it to dark mode. It'll follow the system, so once you notice the website changed to light mode, toggle it to dark mode again.

Top comments (0)