Web themes are a feature that has become more popular in recent years. They are a predefined set of styles that change the visual interface of the website or application.
Lot of sites supply the user a way for controlling the color scheme. Usually, between two options: dark
or light
.
Potential problems during development"undefined" anchor link
As web developers, we can encounter various problems when implementing this feature: the theme selector. In this blog post, we’ll delve into some of these challenges and discuss potential solutions.
Flickering"undefined" anchor link
One common problem arises when using a component with islands or JavaScript as an asset. This leads to flickering if the selected theme differs from the initial page appearance. To prevent this flickering issue, we shouldn’t compile the JavaScript or pass it through an island.
The workaround involves embedding inline code in the HTML, ensuring it executes immediately upon loading.
User preferences and settings"undefined" anchor link
We should effectively manage three points:
-
The
localStorage
previously saved by the visitor. We should prioritize stored preferences over the rest. In the first visit or when the browser data has been cleaned, it won’t exist. -
The
prefers-color-scheme
value. This is the preference configured in the operating system setting or a user agent setting. It would be our second option. -
The default theme we intend to use if we don’t know the user preference. This one will be the last option.
Example Astro code"undefined" anchor link
Here you can see a functional theme selector component code example.
It will handle the user preferences and then initialize the theme. This initialization consists on adding a class to the body .theme-dark
.
Also, it will add a handler to the button
that will persist this selection in the localStorage
.
<button id='theme-toggle' type='button'> <span class='light-mode'>Set light mode 🌞</span> <span class='dark-mode'>Set dark mode 🌑</span></button>
<script is:inline> // Inline: not bundled. It will execute as soon as possible const STORAGE_THEME_KEY = 'theme' const DARK_THEME_CLASS = 'theme-dark' const DARK = 'dark' const LIGHT = 'light' const root = document.documentElement let isDark = true
const getUserPreferences = (localStorageKey) => { if (localStorage && localStorage.getItem(localStorageKey)) return localStorage.getItem(localStorageKey) if (window.matchMedia('(prefers-color-scheme: light)').matches) return LIGHT return DARK }
function initTheme() { const theme = getUserPreferences(STORAGE_THEME_KEY) isDark = theme === DARK root.classList.toggle(DARK_THEME_CLASS, isDark) }
function handleClick() { isDark = !isDark
root.classList.toggle(DARK_THEME_CLASS, isDark) localStorage.setItem(STORAGE_THEME_KEY, isDark ? DARK : LIGHT) }
function setButtonHandler() { const button = document.getElementById('theme-toggle') button?.addEventListener('click', () => handleClick()) }
// Initial navigation initTheme() setButtonHandler()</script>
<style is:global> .light-mode, .theme-dark .dark-mode { display: flex; }
.theme-dark .light-mode, .dark-mode { display: none; }</style>
If you are using Astro ViewTransitions component, you should add few lines:
<button id='theme-toggle' type='button'> <span class='light-mode'>Set light mode 🌞</span> <span class='dark-mode'>Set dark mode 🌑</span></button>
<script is:inline> // Inline: not bundled. It will execute as soon as possible const STORAGE_THEME_KEY = 'theme' const DARK_THEME_CLASS = 'theme-dark' const DARK = 'dark' const LIGHT = 'light' const root = document.documentElement let isDark = true
const getUserPreferences = (localStorageKey) => { if (localStorage && localStorage.getItem(localStorageKey)) return localStorage.getItem(localStorageKey) if (window.matchMedia('(prefers-color-scheme: light)').matches) return LIGHT return DARK }
function initTheme() { const theme = getUserPreferences(STORAGE_THEME_KEY) isDark = theme === DARK root.classList.toggle(DARK_THEME_CLASS, isDark) }
function handleClick() { isDark = !isDark
root.classList.toggle(DARK_THEME_CLASS, isDark) localStorage.setItem(STORAGE_THEME_KEY, isDark ? DARK : LIGHT) }
function setButtonHandler() { const button = document.getElementById('theme-toggle') button?.addEventListener('click', () => handleClick()) }
// Initial navigation initTheme() setButtonHandler()
// On view transitions navigation document.addEventListener('astro:after-swap', () => { initTheme() setButtonHandler() })</script>
<style is:global> .light-mode, .theme-dark .dark-mode { display: flex; }
.theme-dark .light-mode, .dark-mode { display: none; }</style>
Styles"undefined" anchor link
Finally, you only have to add your styles. I plenty suggest managing them through CSS custom properties.
:root { --color-text: #f7f7f7; --color-background: #1c1c1c; /* Add your light tokens here */}
.theme-dark { --color-text: #1c1c1c; --color-background: #f7f7f7; /* Add your dark tokens here */}
Conclusion"undefined" anchor link
Adding a theme selector feature in a web could be painless if you take into account flickering issues and user preferences persistance.
By the way, you have to consider if adding one will provide any value to your project. Maintaining multiple themes involves an effort, since you have to ensure a consistent user experience and a high accessibility. Always decide if it’s worth all the extra work compared to the value it provides.
I hope you found this article useful.
Happy coding! 🚀