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
, 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 thesetTheme
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 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
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 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');
}
});
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.
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
Top comments (1)
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