How to Add Dark Mode to Your Website

Ashal Farhan
6 min readJan 21, 2024

--

Photo by Mario Azzi on Unsplash

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

In the above snippet, we declared 2 variables —-bg-color and —-color, and 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>

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

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
}

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

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>

Here we are reading from the localStorage and checking if there’s no value from the localStorage with the key of "theme" (which means this is the first time the user visited 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

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

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>

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 follow whatever system preferences they currently choose.

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

Helpful links

--

--

No responses yet