ph/components

Theme toggle

Light and dark mode switch. Put ThemeScript in <head>.

Code

Copy into src/components/ph/ .

theme.ts
export type Theme = "light" | "dark";

const KEY = "ph-theme";

export function getTheme(): Theme {
  try {
    const saved = localStorage.getItem(KEY);
    if (saved === "light" || saved === "dark") return saved;
  } catch {}
  return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}

export function applyTheme(theme: Theme) {
  document.documentElement.classList.toggle("dark", theme === "dark");
}

export function setTheme(theme: Theme | null) {
  try {
    if (theme) localStorage.setItem(KEY, theme);
    else localStorage.removeItem(KEY);
  } catch {}
  applyTheme(getTheme());
}

// Inline in <head> so the page never flashes the wrong theme.
export const themeScript = `try{var t=localStorage.getItem("${KEY}");if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.classList.toggle("dark",t==="dark")}catch(e){}`;
ThemeToggle.astro
---
import Icon from "./Icon.astro";

type Props = {
  class?: string;
};

const { class: className } = Astro.props;
---

<button
  type="button"
  data-ph-theme-toggle
  aria-label="Toggle dark mode"
  title="Middle-click to follow your system"
  class:list={["inline-flex cursor-pointer items-center justify-center p-1.5 transition-transform duration-100 active:scale-[0.97]", className]}
>
  <Icon name="sun" class="size-4.5 dark:hidden" />
  <Icon name="moon" class="hidden size-4 -rotate-6 dark:block" />
</button>

<script>
  import { applyTheme, getTheme, setTheme } from "./theme";

  for (const b of document.querySelectorAll<HTMLButtonElement>("[data-ph-theme-toggle]")) {
    b.addEventListener("click", () => setTheme(getTheme() === "dark" ? "light" : "dark"));
    b.addEventListener("auxclick", (e) => {
      if (e.button === 1) setTheme(null);
    });
  }

  matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => applyTheme(getTheme()));
</script>
ThemeScript.astro
---
import { themeScript } from "./theme";
---

<script is:inline set:html={themeScript} />

Also needs