163 lines
No EOL
4.7 KiB
Markdown
163 lines
No EOL
4.7 KiB
Markdown
---
|
|
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).<!-- excerpt -->
|
|
|
|
I built the button as a short Liquid partial:
|
|
|
|
{% raw %}
|
|
```liquid
|
|
<li class="theme-toggle client-side">
|
|
<span class="placeholder">
|
|
{% tablericon "placeholder" "Toggle theme placeholder" %}
|
|
</span>
|
|
<span class="light">
|
|
{% tablericon "sun" "Toggle light theme" %}
|
|
</span>
|
|
<span class="dark">
|
|
{% tablericon "moon" "Toggle dark theme" %}
|
|
</span>
|
|
</li>
|
|
```
|
|
{% endraw %}
|
|
|
|
The `client-side` class above hides the button should the user have JavaScript disabled:
|
|
|
|
{% raw %]}
|
|
```html
|
|
<noscript>
|
|
<style>
|
|
.client-side {
|
|
display: none
|
|
}
|
|
</style>
|
|
</noscript>
|
|
```
|
|
{% endraw %}
|
|
|
|
And JavaScript is used to control the behavior of the toggle — first, in the `<body>` of my base template:
|
|
|
|
{% raw %}
|
|
```liquid
|
|
{% capture js %}
|
|
{% render "../assets/scripts/theme.js" %}
|
|
{% endcapture %}
|
|
<script>{{ js }}</script>
|
|
```
|
|
{% 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). |