Light / Dark mode toggle
A light and dark mode toggle that lets visitors override their system preference, with the choice remembered on their next visit.1
Preview

A small icon button (sun in dark mode, moon in light mode) sitting in the nav. Clicking it flips the whole palette instantly, no page reload.
How to use
Markup
Add the button wherever you want the toggle to live, for example in your nav:
<button id="theme-toggle" aria-label="Toggle color theme">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
Script
In your Header directive, add this small script:
<script>
(function() {
var t = localStorage.getItem('theme');
if (t) document.documentElement.setAttribute('data-theme', t);
})();
</script>
In your Footer directive, add the toggle logic:
<script>
(function() {
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
function currentTheme() {
const attr = document.documentElement.getAttribute('data-theme');
if (attr) return attr;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const toggle = document.getElementById('theme-toggle');
if (toggle) {
toggle.addEventListener('click', () => {
setTheme(currentTheme() === 'dark' ? 'light' : 'dark');
});
}
})();
</script>
Styles
Add the following styles to your theme.
/* Toggle */
.icon-sun, .icon-moon {
display: none;
width: 1.1em;
height: 1.1em;
color: var(--muted);
vertical-align: -0.15em;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme]) .icon-moon {
display: inline;
}
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .icon-sun {
display: inline;
}
}
:root[data-theme="light"] .icon-moon {
display: inline;
}
:root[data-theme="dark"] .icon-sun {
display: inline;
}
#theme-toggle {
padding: 0;
border: none;
background: none;
color: inherit;
vertical-align: middle;
}
@media (hover: hover) {
#theme-toggle:hover {
background: none;
border-color: transparent;
}
}
Updating your palette
Your palette's :root block needs two small changes so the toggle has something to switch.
Here's a typical palette before the toggle is added:
:root {
--bg: #f8f8f8;
--text: #2c2d2e;
--link: #2952cc;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1c1f23;
--text: #d9dbdd;
--link: #74b0f4;
}
}
And here's the same palette after:
:root {
--bg: #f8f8f8;
--text: #2c2d2e;
--link: #2952cc;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg: #1c1f23;
--text: #d9dbdd;
--link: #74b0f4;
}
}
:root[data-theme="dark"] {
--bg: #1c1f23;
--text: #d9dbdd;
--link: #74b0f4;
}
:root[data-theme="light"] {
--bg: #f8f8f8;
--text: #2c2d2e;
--link: #2952cc;
}
Two changes, that's it:
:not([data-theme])added to the existing dark media query, so it only applies when the visitor hasn't clicked the toggle.- Two new blocks at the bottom,
[data-theme="dark"]and[data-theme="light"], repeating the same values. These are what the button actually switches between.
Apply the same pattern to the rest of your palette variables (--accent, --muted, --surface, --border, and so on), just repeat each one in both new blocks with its light and dark values.
Want more? Check out all available Bearming add-ons.