Skip to content

Light and Dark Scheme for Modern Websites

by Christian Praß

Back in the olden days it required a lot of JS to add a dark and light scheme toggle for a website. Now it’s a built-in feature for all modern browsers and operating systems and requires no JavaScript at all, unless a manual toggle button is wanted.

CSS Only

Most operating systems have internal dark and light schemes and are able to tell the browser and the websites what system scheme is currently active. Browsers also have their own default styles for each scheme and it’s not required to set any colors manually. One line of CSS is all that’s needed.

  :root {
    color-scheme: light dark;
  }

Alternatively, a HTML meta tag can be used.

<meta name="color-scheme" content="light dark">

If the user enables dark mode in the OS the website will have a dark scheme. Switching back to light mode in the OS also switches the website to light scheme. When using custom colors a media query can be used to detect the preferred system color scheme.

:root {
  --background: #fefefe;
  --foreground: #1b1c21;
  --accent: #12558a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #282A36;
    --foreground: #F8F8F2;
    --accent: #8BE9FD;
  }
}

With a Theme Toggle and Classes

Sometimes a website uses a light/dark button to allow users to toggle the scheme manually, independent of the OS. This can be achieved with some really simpe JS code.

<button data-theme-toggle title="toggle theme" type="button">
  <span class="icon icon-sun" aria-label="toggle dark theme" />
  <span class="icon icon-moon" aria-label="toggle light theme" />
</button>

<script>
  // add or remove the class "dark" in the root element
  const cl = document.documentElement.classList;
  function toggleTheme() {
    const isDark = cl.contains("dark");
    cl[isDark ? 'remove' : 'add']("dark");
  }

  document
    .querySelector('[data-theme-toggle]')
    ?.addEventListener('click', () => {
      toggleTheme();
    });
</script>

<style>
  :root {
    --background: #fefefe;
    /* ... */
  }
  :root.dark {
    --background: #282A36;
    /* ... */
  }

  :root.dark .sun {
    display: none;
  }
  :root .moon {
    display: none;
  }
  :root.dark .moon {
    display: unset;
  }
</style>

To make the page set the correct class on the root element on page load, add a script inside of the <head> element of the page. This is necessary to avoid a light theme flashing on page load.

<head>
  <script>
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>

Storing the Selected Theme in Local-Storage

The problem with the manual toggle is that the page will set the default system theme on page load. This is even more problematic on static pages, where every link resets the manually toggled scheme. The solution to this problem is to store the setting in the browsers local storage. If users visit the page after they manually toggled the scheme before, the browser will apply the previous scheme, even if the system has a different default scheme.

function toggleTheme() {
  // ...
  localStorage.setItem("theme", isDark ? "light" : "dark");
}

This requires small changes to the scheme loader script in the page head.

<head>
  <script>
    const t = localStorage.getItem('theme') ?? null;
    if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>

Issues in Astro

For Astro pages, to avoid scheme flashing on page load it is important to tell Astro not to optimize and bundle the scheme loader script. This can be achieved with the is:inline directive.

<head>
  <script is:inline>
    const t = localStorage.getItem('theme') ?? null;
    if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>

I’m currently using the local-storage backed version with manual theme toggle in addition to detecting the system color scheme. I’m going to remove that toggle button and only use the system scheme, getting rid of all additional JavaScript code. The CSS only version completely avoids the scheme flashing issue and makes the website a tiny bit faster and lighter. Dark and light mode toggles are so easy to reach in the OS nowadays. A manual toggle button on the website seems obsolete.