DEV Community

Cover image for From Light to Night: A Comprehensive Guide to Dark Mode Implementation
Ashal Farhan
Ashal Farhan

Posted on • Edited on

From Light to Night: A Comprehensive Guide to Dark Mode Implementation

In this article, we will take a minimalist approach, focusing on the implementation of dark mode using the inherent capabilities of web browsers. By harnessing native features, developers can seamlessly integrate dark mode into their applications without relying on third-party libraries or complex solutions.

Native Browser Features for Dark Mode

There are some of the native browser features that we can utilise to implement dark mode.

CSS Variables

Or sometimes called CSS Custom Properties. We can use this feature to store some of the colour palettes so that we don't have to repeatedly specify the hex or the rgba value.

Let's create a stylesheet file styles.css that will look something like this:

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

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

body {
  background-color: var(--bg-color);
  color: var(--color);
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet, we declared 2 variables --bg-color and --color, we set them to white and black. 

Below that, we declare the same variables but with reversed colours. 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 them to the body element. With this, all we need to do is to add data-theme attribute set to "dark" with JavaScript.

JavaScript for Dynamic Dark Mode

Before we start to get our hands dirty with scripting, let's create the HTML file, and reference the stylesheet that we've been created earlier.

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

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

Now here's the fun part, create a script file script.js:

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

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

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

In the 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 the HTML's data-theme attribute and change the 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.
  • Check if the current theme is "dark", then we call the setTheme function and pass "light" as the argument, otherwise pass "dark".

✨ Great! Now you should be able to toggle the theme.

Real-world scenarios

Now let's improve the current implementation based on real-world scenarios.

Persisting

To persist the user-selected theme, we can use the Web Storage called localStorage. It's a simple persistence storage that is tied to our site's domain.

What we need to do is to save the selected theme to the storage 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';
  themeButton.innerText = `Switch to ${text}`;
  localStorage.setItem('theme', theme); // Add this line
}
Enter fullscreen mode Exit fullscreen mode

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

// 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
Enter fullscreen mode Exit fullscreen mode

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

Try that out by toggling the theme to dark and then reloading the page.

Prevent FUOC

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

FUOC stands for "Flash of Unstyled Content".

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Color Scheme

We can also utilise the color-scheme CSS property to hint the browser about the colour scheme of our site. The common values for this property are dark and light. This property will also change our initial element styling including form controls, scrollbars, etc.

What we need to do is to set this property to the HTML document whenever the user changes 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);
}
Enter fullscreen mode Exit fullscreen mode

Then we also need to 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>
Enter fullscreen mode Exit fullscreen mode

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

Reacting to system preferences changes

The last tip is to make our site respond to the device's system preference. Whenever the user changes their system preferences, we will make our site also follow 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');
  }
});
Enter fullscreen mode Exit fullscreen mode

Here we are listening for a change event of the CSS prefer-color-scheme media query, then we check if the event matches (which means the user's system preference is on the dark mode), and 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 reacts to that.

Conclusion

As we wrap up our exploration of dark mode implementation, it's evident that the native features of web browsers offer a powerful toolkit for developers. By leveraging CSS variables and a touch of JavaScript, we can seamlessly introduce dark mode to our applications. This minimalist approach not only enhances user experience but also contributes to efficient and lightweight code.

Here's a working codesandbox

Helpful links

Top comments (1)

Collapse
 
monicafidalgo profile image
The Coding Mermaid 🧜‍♀️

Wow! Congrats on this post! You did put a lot of effort into writing all of this and you explain it very well! You explain how to start, how to improve.. and why.. also the links in the end! Thanks a lot for this :) I will definitely try it one day