All files / src/components/theme ThemeToggle.tsx

58.33% Statements 7/12
50% Branches 5/10
66.66% Functions 2/3
58.33% Lines 7/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 457x                   7x 66x         29x     29x 29x                         29x                      
"use client";
 
import { useState } from "react";
import { useDeviceValue } from "@/lib/useDeviceValue";
 
type Theme = "light" | "dark";
 
// ThemeScript writes data-theme on <html> before paint, so the attribute — not this
// component — is the source of truth on arrival. It is device-local (invisible to the
// server render), hence the useDeviceValue read.
const readTheme = (): Theme =>
  document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
 
// Flips data-theme on <html> and persists the choice. Reads the current value
// set by ThemeScript so it stays in sync on first paint.
export function ThemeToggle() {
  const painted = useDeviceValue<Theme>(readTheme, "light");
  // The flip is remembered here rather than re-read from the DOM: a failed
  // localStorage write (private mode) must still leave the toggle in its new state.
  const [flipped, setFlipped] = useState<Theme | null>(null);
  const theme = flipped ?? painted;
 
  function flip() {
    const next: Theme = theme === "dark" ? "light" : "dark";
    document.documentElement.setAttribute("data-theme", next);
    try {
      localStorage.setItem("pi-theme", next);
    } catch {
      // private mode / storage disabled — the in-memory toggle still works
    }
    setFlipped(next);
  }
 
  return (
    <button
      type="button"
      onClick={flip}
      aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} theme`}
      className="inline-flex h-11 min-w-11 items-center justify-center rounded-control px-3 text-ink transition-colors hover:bg-content-soft"
    >
      <span aria-hidden="true">{theme === "dark" ? "☾" : "☀"}</span>
    </button>
  );
}