Vanilla Dark Mode


Saturday, June 11, 2022 - 6 min read
Vanilla Dark Mode
Vanilla Dark Mode

Photo by Mario Azzi

If you're wondering: "How can I add dark mode without any javascript framework or library?" then the answer is: "Yes, you can!".

Assuming that you have some understanding in CSS, and JavaScript, let's dive into it.

Adaptive stylesheet

Create a style.css, and copy this code

html {
  --bg-color: #fff;
  --color: #000;
}

html[data-theme='dark'] {
  --bg-color: #000;
  --color: #fff;
}

body {
  background-color: var(--bg-color);
  color: var(--color);
}

In above snippet, we declare 2 variables --bg-color and --color and set them to white and black. Below that, we declare the same variables with reversed colors. The second selector means: if the html document has data-theme attribute set to dark, then modify both variables with the value inside of the html[data-theme='dark'] block.

Then we use the variables by applying that to the body element.

Controlling the theme

After stylesheet created, then we can continue to add a button to toggle the theme.

<!-- index.html -->
<head>
  <link rel="stylesheet" href="styles.css" />
</head>
<body>
  <nav>
    <button id="theme-button">Switch to dark</button>
  </nav>
</body>

This button will responsible for toggling the data-theme attribute of the html document.

Now let's take a look what we need to do with JavaScript.

// script.js
const themeButtom = document.getElementById('theme-button');

function setTheme(theme = 'light') {
  document.documentElement.setAttribute('data-theme', theme);
  const text = theme === 'light' ? 'dark' : 'light';
  themeButtom.innerText = `Switch to ${text}`;
}

themeButtom.addEventListener('click', () => {
  const currentTheme = document.documentElement.getAttribute('data-theme');
  if (currentTheme === 'dark') {
    setTheme('light');
  } else {
    setTheme('dark');
  }
});

In above snippet, we grab the toggle theme button and store it in a variable called themeButton, and we declare a function called setTheme to change html's data-theme attribute, and our theme button's text to the opposite of the selected theme.

Then we're listening to the button's click event, and what we're doing is:

  • Get the current theme that is retrieved from the html's data-theme attribute.
  • And we check if the current theme is "dark", then we call the setTheme function with "light" argument, otherwise pass "dark".

Congratulations!, now you should be able to toggle the theme.

Bonus

Here are some bonus tips for you.

Persisting

To persist the user selected theme, we can use the Web Storage called localStorage or sessionStorage.

You can read more about them here.

In short, localStorage will keep the value forever, unless user decided to remove it. On the other hand, sessionStorage will remove all of the value if user close the browser.

For this example I think localStorage is the best option to save the theme.

What we need to do is to save the selected theme every time we call the setTheme function

// script.js
//... rest of the code

function setTheme(theme = 'light') {
  document.documentElement.setAttribute('data-theme', theme);
  const text = theme === 'light' ? 'dark' : 'light';
  themeButtom.innerText = `Switch to ${text}`;
  localStorage.setItem('theme', theme); // Add this line
}

After we save the selected theme, we can load the selected theme from the localStorage when the page load.

// script.js
//... rest of the code
function setTheme(theme = 'light') {
  document.documentElement.setAttribute('data-theme', theme);
  const text = theme === 'light' ? 'dark' : 'light';
  themeButtom.innerText = `Switch to ${text}`;
  localStorage.setItem('theme', theme);
}

// We read from the `localStorage`
// If there's nothing saved, then the fallback will be `"light"`
const preloadedTheme = localStorage.getItem('theme') || 'light';

// Immediately call `setTheme`
setTheme(preloadedTheme);
//... rest of the code

At this point, your site's theme preference should already be persisted.

Prevent flashing

If you try to set the theme to dark and reload the page, you should notice some short of flashing. This is because the script that has the logic to set the theme from localStorage is executed after the first browser paint.

Preload

To solve this, we need to move the logic of reading from localStorage to the head of the html document.

<head>
  <script>
    let preloadedTheme = localStorage.getItem('theme');
    if (preloadedTheme == null) {
      const isPreferDark = window.matchMedia(
        '(prefers-color-scheme: dark)',
      ).matches;
      preloadedTheme = isPreferDark ? 'dark' : 'light';
    }
    document.documentElement.setAttribute('data-theme', preloadedTheme);
  </script>
  <!-- ... rest of the head -->
</head>

Here we are reading from the localStorage and check if there's no value from the localStorage with the key of 'theme' (which means this is the first time the user visit our site) then we try to detect their system preference by using window.matchMedia method, and set the data-theme to whatever the system preference is. We are saving this to the preloadedTheme variable, and now we can remove the step of reading localStorage in our script.

// script.js
const preloadedTheme = localStorage.getItem('theme') || 'light'; // Remove this line

// The `preloadedTheme` variable is coming from the head of our preload script
setTheme(preloadedTheme);
//... rest of the code

Color Scheme

This is the last optimization if your site have a heavy content.

We can utilize the color-scheme CSS property so that we can give a hint to the browser about the color scheme of our site. The common value for this property are dark and light. This property will also change our initial element styling including form controls, and scrollbars.

What we need to do is to set this property to the html document whenever the user change the theme.

// script.js
//... rest of the code
function setTheme(theme = 'light') {
  document.documentElement.setAttribute('data-theme', theme);
  document.documentElement.style['color-scheme'] = theme; // Add this line
  const text = theme === 'light' ? 'dark' : 'light';
  themeButtom.innerText = `Switch to ${text}`;
  localStorage.setItem('theme', theme);
}

Then we can also a set this property in the preload script.

<head>
  <script>
    let preloadedTheme = localStorage.getItem('theme');
    if (preloadedTheme == null) {
      const isPreferDark = window.matchMedia(
        '(prefers-color-scheme: dark)',
      ).matches;
      preloadedTheme = isPreferDark ? 'dark' : 'light';
    }
    document.documentElement.setAttribute('data-theme', preloadedTheme);
    document.documentElement.style['color-scheme'] = preloadedTheme; // Add this line
  </script>
  <!-- ... rest of the head -->
</head>

And now you shouldn't get that flashing anymore, Cool!

Reacting to system preferences changes

The last bonus is to make our site respond to the system preferences. Whenever the user change their system preferences, we will make our site also following whatever the system preferences that they currently chose.

// script.js
// ... rest of the code

const darkMode = window.matchMedia('(prefers-color-scheme: dark)');
darkMode.addEventListener('change', e => {
  if (e.matches) {
    setTheme('dark');
  } else {
    setTheme('light');
  }
});

Here we are listening for change event of the CSS prefers-color-scheme media query, then we check if the event matches (which means user's system preference is on the dark mode), then change our site's theme to dark.

To test this, you can change the system preference of your device and make sure that your site will also follow whatever the system preference that you chose.

Conclusion

I hope this is useful to you!

Here's a working codesandbox