Astro docs suggest implementing a dark mode like so.
That’s great and works fine for initial implementation but runs into issues when using the ClientRouter for those j00cy bult in page transitions.
Namely it’s great toggling on first load but falls flat after navigating to different pages on your site until you hit the hard server rendered refreshed (that’s F5 to you and me).
With some workarounds you can get the best of all worlds - great light/dark (why stop at two?) themes to toggle between, persistence between client side page routes, beautiful Astro page transitions AND respecting the user’s prefers-color-scheme: dark
preference set in browser/OS/cave.
tldr here’s the code for the ThemeIcon.astro component (I put mine in src/compnents/ThemeIcon.astro
) and the sitewide import in the BaseLayout.
---
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
<span id="theme-toggle-dark-icon">☀️</span>
<span id="theme-toggle-light-icon">🌙</span>
<span id="theme-toggle-terminal-icon">🤖</span>
</button>
<style>
.theme-toggle {
border: none;
background: none;
cursor: pointer;
padding: 0;
font-size: 1.2rem;
}
.hidden {
display: none;
}
</style>
<script is:inline>
const STORAGE_KEY = "theme";
const THEMES = ["light", "dark", "terminal"];
// Function to handle theme updates
function updateTheme(newTheme) {
// Update document classes
document.documentElement.classList.remove(...THEMES);
document.documentElement.classList.add(newTheme);
// Update icons
const darkIcon = document.getElementById("theme-toggle-dark-icon");
const lightIcon = document.getElementById("theme-toggle-light-icon");
const terminalIcon = document.getElementById("theme-toggle-terminal-icon");
if (darkIcon && lightIcon && terminalIcon) {
darkIcon.classList.toggle("hidden", newTheme !== "light");
lightIcon.classList.toggle("hidden", newTheme !== "dark");
terminalIcon.classList.toggle("hidden", newTheme !== "terminal");
}
// Store the state
localStorage.setItem(STORAGE_KEY, newTheme);
}
function getNextTheme(currentTheme) {
const currentIndex = THEMES.indexOf(currentTheme);
return THEMES[(currentIndex + 1) % THEMES.length];
}
// Function to setup theme toggle
function setupTheme() {
// Get initial theme
const theme = (() => {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem(STORAGE_KEY)
) {
return localStorage.getItem(STORAGE_KEY);
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
})();
// Apply initial theme
updateTheme(theme);
// Setup click handler
const toggle = document.getElementById("theme-toggle");
if (toggle) {
toggle.addEventListener("click", () => {
const currentTheme = localStorage.getItem(STORAGE_KEY) || "light";
const nextTheme = getNextTheme(currentTheme);
updateTheme(nextTheme);
});
}
}
// Initial setup
setupTheme();
// Handle Astro view transitions
document.addEventListener("astro:after-swap", () => {
setupTheme();
});
// Handle system theme changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (!localStorage.getItem(STORAGE_KEY)) {
updateTheme(e.matches ? "dark" : "light");
}
});
</script>
Then chuck that in your main site layout - for me that’s in the nav in BaseLayout.astro in src/layouts/BaseLayout.astro
This is also where you would declare the page transitions with the ClientRouter
import and placement in head:
---
const { title } = Astro.props;
import { ClientRouter } from "astro:transitions";
import ThemeIcon from '../components/ThemeIcon.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<ClientRouter />
</head>
<body>
<div class="container">
// I <3 the new popover API
// But you might want to do the nav with JS for total browser compatibility
<button
popovertarget="menu"
popovertargetaction="toggle"
class="menu-trigger">☰</button
>
<nav id="menu" popover="auto" class="menu-content">
<a href="/">👀 home</a>
<a href="/about">📖 about</a>
<a href="/stuff">🏗 stuff</a>
<a href="/words">💬 words</a>
<ThemeIcon />
</nav>
<slot />
</div>
</body>
</html>
<style>
html {
--bg-color: #ecf1f5;
--text-color: black;
--menu-bg: rgba(255, 255, 255, 0.2);
}
html.dark {
--bg-color: #1a1625;
--text-color: #ffffff;
--menu-bg: rgba(0, 0, 0, 0.2);
}
html.terminal {
--bg-color: #000000;
--text-color: #00ff00;
--menu-bg: rgba(0, 255, 0, 0.1);
}
html,
body {
height: 100%;
margin: 0;
font-family: "Courier New", Courier, monospace;
color: var(--text-color);
}
//rest of your BaseLayout styles
</style>
Then adjust your css colours for each html.theme as you see fit.
That’s about it - thanks for reading! alcun ✌️