From Light to Night: A Comprehensive Guide to Dark Mode Implementation
Tags: javascript, css, webdev, darkmode
Published: 2022-06-11 04:02:20

---
title: From Light to Night: A Comprehensive Guide to Dark Mode Implementation
published: true
description: A guide how to add dark mode in a website with only CSS and JavaScript
tags:
 - javascript
 - css
 - webdev
 - darkmode
cover_image: https://ik.imagekit.io/gncpb3rwf/posts/mario-azzi-CL6560K5vOo-unsplash_G29pe5Xu6.jpg
---

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:

```css
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 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.

```html
<!-- 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 be responsible for toggling the `data-theme` attribute of the HTML document.

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

```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');
  }
});
```

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.

```js
// 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
}
```

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

```js
// 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.

> 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.

```html
<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.

```js
// 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

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.

```js
// 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 also need to set this property in the preload script.

```html
<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 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.

```js
// 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 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](https://codesandbox.io/s/dark-mode-with-css-and-js-69uv6t)

## Helpful links

- CSS Variables: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
- CSS `color-scheme`: https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme
- `window.matchMedia`: https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia