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 โœŒ๏ธ