--- date: '2024-02-17' title: 'Adding a light-dark theme toggle' description: "I dropped a light/dark theme toggle into the navigation of my site, replacing the prior reliance on the visitor's preference set at the OS level (though it does still consider this preference)." tags: ['CSS', 'javascript', 'Eleventy', 'development'] --- I dropped a light/dark theme toggle into the navigation of my site, replacing the prior reliance on the visitor's preference set at the OS level (though it does still consider this preference). I built the button as a short Liquid partial: {% raw %} ```liquid
  • {% tablericon "placeholder" "Toggle theme placeholder" %} {% tablericon "sun" "Toggle light theme" %} {% tablericon "moon" "Toggle dark theme" %}
  • ``` {% endraw %} The `client-side` class above hides the button should the user have JavaScript disabled: {% raw %]} ```html ``` {% endraw %} And JavaScript is used to control the behavior of the toggle — first, in the `` of my base template: {% raw %} ```liquid {% capture js %} {% render "../assets/scripts/theme.js" %} {% endcapture %} ``` {% endraw %} Where `theme.js` sets the initial state: ```javascript ;(async function() { const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; const currentTheme = localStorage?.getItem('theme'); if (currentTheme === 'dark') { document.body.classList.add('theme__dark'); } else if (currentTheme === 'light') { document.body.classList.add('theme__light'); } else if (prefersDarkScheme) { document.body.classList.add('theme__dark'); } else if (!prefersDarkScheme) { document.body.classList.add('theme__light'); } })() ``` With the following JavaScript set in `assets/scripts/index.js` to add the `click` event listener for the theme toggle: ```javascript ;(async function() { const btn = document.querySelector('.theme-toggle'); btn.addEventListener('click', () => { document.body.classList.toggle('theme__light'); document.body.classList.toggle('theme__dark'); const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; const currentTheme = localStorage?.getItem('theme'); let theme; if (!currentTheme) localStorage?.setItem('theme', (prefersDarkScheme ? 'dark' : 'light')) if (prefersDarkScheme) { theme = document.body.classList.contains('theme__light') ? 'light' : 'dark'; } else { theme = document.body.classList.contains('theme__dark') ? 'dark' : 'light'; } localStorage?.setItem('theme', theme); }); })() ``` Finally, the theme is updated based on the body class applied by the JavaScript, updating the variable values that define my site's theme: ```css /* theme toggle */ .theme-toggle, .theme-toggle svg { cursor: pointer; } .theme-toggle > .light svg { stroke: var(--sun) !important; } .theme-toggle > .dark svg { stroke: var(--moon) !important; } .theme-toggle > .light , .theme-toggle > .dark { display: none; } :is(.theme__light, .theme__dark) .theme-toggle > .placeholder { display: none; } .theme__dark .theme-toggle > .light { display: inline; } .theme__dark .theme-toggle > .dark { display: none; } .theme__light .theme-toggle > .light { display: none; } .theme__light .theme-toggle > .dark { display: inline; } ... :root body.theme__light { --text-color: var(--color-darkest); --background-color: var(--color-lightest); --text-color-inverted: var(--color-lightest); --background-color-inverted: var(--color-darkest); --accent-color: var(--blue-600); --accent-color-hover: var(--blue-800); --selection-color: var(--accent-color); --gray-light: var(--gray-200); --gray-lighter: var(--gray-50); --gray-dark: var(--gray-700); --brand-github: #333; } :root body.theme__dark { --text-color: var(--color-lightest); --background-color: var(--color-darkest); --text-color-inverted: var(--color-darkest); --background-color-inverted: var(--color-lightest); --accent-color: var(--blue-400); --accent-color-hover: var(--blue-200); --gray-light: var(--gray-900); --gray-lighter: var(--gray-950); --gray-dark: var(--gray-300); --brand-github: #f5f5f5; } ``` With those changes in place, visitors can toggle whichever theme they'd prefer and their preference is persisted in `localStorage` should it be available. **EDIT:** Many thanks to [Tixie Salander](https://mastodon.guerilla.studio/@tixie) for [guiding me to a solution that improves the initial load experience](https://mastodon.guerilla.studio/@tixie/111950371813634672).