PSA: Add dark mode to your sites, or at least let the browsers do it for you

I have a simple message for web developers, start adding the color-scheme property to your webpages.

<!--
  The page supports both dark and light color schemes,
  and the page author prefers dark.
-->
<meta name="color-scheme" content="dark light">

or you can even add it using css

/*
  The page supports both dark and light color schemes,
  and the page author prefers dark.
*/
:root {
  color-scheme: dark light;
}

I absolutely detest sites that "have a dark mode, BUT DON'T MAKE THE SCROLLBAR DARK!", a great example of this is docusaurus.

Docusarus Why???

I actually tweeted at them asking why the scrollbar isn't dark as well, šŸ¤£

The light mode scrollbar hurts the eyes, and ruins the look of the site, so, for the sake of everyone who has eyes and likes dark mode, please use color-scheme, you can even use it together with your dark mode toggle by using css, for example, one of the sites I made for a client josephojo.com

When using the color-scheme property you can turn form elements, webpage background, text color, and scrollbars dark, a more famous example would be, github,

Notice, how the scrollbar is dark, and doesn't burn the eyes, they're able to do it, by using the meta tag.

For josephojo.com I used the color-scheme css property together with @media (prefers-color-scheme: dark) {} and the .dark class, the final result is

html {
    color-scheme: light;
}

html.dark {
    color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
    html:not([data-theme]) {
        color-scheme: dark;
    }
}

When creating the site I used tailwindcss, with the dark mode set to "class", my tailwind config looked like this,

module.exports = {
    darkMode: 'class',
    // ...
}

For those who haven't used tailwindcss before, it's basically the same as defining a class that when added to the html element will signal that the site is in dark mode.

Or in simpler terms it's,

<html class="dark"> 
    <!-- ... -->
</html>

You: Wait, but, how did you handle the theme toggle button?

Me: I'm glad you asked.

Now that we have some boilerplate code, all you really need to do, is setup a toggle that will remember our current theme state.

While developing josephojo.com, I found that you have to set your theming system to support the native media theme before anything else, it's generally less painful to the user, that's why I set html:not([data-theme]) in the prefers-color-scheme: dark media query,

/* ... */
@media (prefers-color-scheme: dark) {
    html:not([data-theme]) {
        color-scheme: dark;
    }
}
/* ... */

html.dark represents the dark theme applied by tailwind and [data-theme] represents the currently applied theme, if data-theme is different from the local storage, then the theme was manually toggled and the page should use the new theme in data-theme as well as update the local storage theme, otherwise, it should use the local storage theme as data-theme, but because data-theme is only applied to the html element after javascript is loaded we can tell our css to use the default dark theme if prefers-color-scheme: dark and the html element doesn't have the data-theme attribute.

The result you get is this,

As you saw at the end there, changing the actual browser theme won't permanently change the theme set in local storage, with the idea being, if a user a manually changes the theme they must want to use that theme permanently, otherwise use the system theme.

Here is the code for the theme toggle,

// Based on [joshwcomeau.com/gatsby/dark-mode/]
let getSavedTheme = () => {
  const theme = window.localStorage.getItem("theme");
  // If the user has explicitly chosen light or dark,
  // let's use it. Otherwise, this value will be null.
  if (typeof theme === "string") return theme;

  // If they are using a browser/OS that doesn't support
  // color themes, let's not do anything.
  return null;
};

let saveTheme = (theme) => {
  // If the user has explicitly chosen light or dark, store the default theme
  if (typeof theme === "string")
    window.localStorage.setItem("theme", theme);
};

let mediaTheme = () => {
  // If they haven't been explicitly set, let's check the media query
  const mql = matchMedia("(prefers-color-scheme: dark)");
  const hasMediaQueryPreference = typeof mql.matches === "boolean";
  if (hasMediaQueryPreference) return mql.matches ? "dark" : "light";
};

const html = document.querySelector("html");

// Get theme from html tag, if it has a theme or get it from localStorage
let checkCurrentTheme = () => {
  let themeAttr = html.getAttribute("data-theme");
  if (themeAttr) return themeAttr;

  return getSavedTheme();
};

// Set theme in localStorage, as well as in the html tag
let applyTheme = (theme) => {
  html.className = theme;
  html.setAttribute("data-theme", theme);
};

try {
  // if there is a saved theme in local storage use that,
  // otherwise use `prefer-color-scheme` to set the theme 
  let theme = getSavedTheme();
  if (theme == null) theme = mediaTheme();

  // set the initial theme
  html.setAttribute("data-theme", theme);
  html.classList.add(theme);

  // If a user changes the system/browser/OS theme, update the site theme as well,
  // but don't save the change in local storage
  window
    .matchMedia("(prefers-color-scheme: dark)")
    .addEventListener("change", (e) => {
      applyTheme(e.matches ? "dark" : "light");
    });

  // On theme toggle button click, toggle the page theme between dark and light mode,
  // then save the theme in local storage
  document
    .querySelector("#theme-toggle")
    .addEventListener("click", () => {
      let theme = checkCurrentTheme() === "dark" ? "light" : "dark";
      applyTheme(theme);
      saveTheme(theme);
    });
} catch (e) {
  console.warn("Theming isn't available on this browser.", e);
}

You can view the demo below, but you need to open the demo in a new tab for it to work properly ( note , I don't mean open the entire Code Sandbox in a new tab, I specifically mean the demo. CodeSandbox has disabled local storage, when the demo is attached to rest of the CodeSandbox instance, so, you need to detach the demo from the CodeSandbox instance).

Also, notice, how I never set the text color, background color, scrollbar color or button styles, that's part of the magic of setting color-scheme.

You can read more about color-scheme on web.dev

Please tell me what you think about color-scheme in the comments below.

Update: Another cool part feature of the color-scheme meta tag is that Samsung Internet won't force dark mode on your site if it uses the color-scheme meta tag, from what I can tell Chrome might implement a similar feature in the future. I tweeted about it

You can learn more about this on the Samsung Developers site,

37