:hover
or :active
) is important for mobile.One option was to save users’ dark mode preference as a cookie. A hook would be added to check for a dark mode preference cookie and modify the response to reflect preference. This seemed overly complicated and not performant.
I can’t use onMount()
, afterUpdate()
or most other svelte functions because they’re run after CSS/HTML rendering and would create flashes.
Tailwind makes it easy to generate dark and light classes using the dark:
modifier. By default dark:
is activated by the window’s prefers-color-scheme
setting. To activate dark mode using classes see the Tailwind documentation .
tailwind.config.js` `module.exports = { darkMode: 'class', ... }
Dark mode can now be controlled by toggling a body class (or any other parent class).
document.body.classList.toggle('dark');
Color scheme preference is persisted using local storage.
if (document.body.classList.contains('dark')) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
Unfortunately there isn’t a straightforward method for animating button clicks. To make a real click animation we have to add an animation class to our button, wait for the animation to finish, and then remove that class once the animation has completed.
The MDN web docs has a brilliant solution for doing this.
document.querySelector('#dark-mode-icon').className = '';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.querySelector('#dark-mode-icon').className = 'spin';
});
});
If including the animation class as an inline component style ensure you use the :global()
modifier.
@keyframes spin {
to {
transform: rotate(90deg);
}
}
:global(.spin) {
animation: spin 0.3s;
}
Putting it together, this is what our dark mode toggle function looks like.
function toggleDarkMode() {
document.body.classList.toggle('dark');
if (document.body.classList.contains('dark')) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
document.querySelector('#dark-mode-icon').className = '';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.querySelector('#dark-mode-icon').className = 'spin';
});
});
}
To prevent the flash we need to set the correct class on the body tag before our page content renders. We can do this by running the color scheme check in our app.html
body tag.
My implementation is a slimmed down and less-versatile adaptation of the noflash script included in the react use-dark-mode package.
const themeKey = 'theme';
// If the user has a saved theme preference in local storage use that
if (localStorage.getItem(themeKey)) {
if (localStorage.theme === 'dark') {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
// elif the user has a browser color scheme preference use that
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
localStorage.setItem(themeKey, 'dark');
document.body.classList.add('dark');
// else default to using the dark theme
} else {
localStorage.setItem(themeKey, 'dark');
}
You can now implement a dark mode toggle that applies color theme preferences without flashes of content.