app/lib/theme.tsx (70 lines of code) (raw):

import React, { createContext, useContext, useEffect, useState } from "react"; type Theme = "light" | "dark" | "system"; interface ThemeContextType { theme: Theme; setTheme: (theme: Theme) => void; resolvedTheme: "light" | "dark"; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState<Theme>(() => { // Initialize with a more accurate default on client-side if (typeof window !== "undefined") { const stored = localStorage.getItem("theme") as Theme; if (stored && ["light", "dark", "system"].includes(stored)) { return stored; } } return "system"; }); const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(() => { // Initialize resolved theme to match what the blocking script would set if (typeof window !== "undefined") { const stored = localStorage.getItem("theme") as Theme; const systemDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; return stored === "dark" || (stored !== "light" && systemDark) ? "dark" : "light"; } return "light"; }); useEffect(() => { const updateResolvedTheme = () => { let resolved: "light" | "dark"; if (theme === "system") { resolved = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } else { resolved = theme; } // Only update if different to avoid unnecessary re-renders if (resolved !== resolvedTheme) { setResolvedTheme(resolved); } // Apply the theme to the document (defensive check) const root = document.documentElement; if (resolved === "dark") { root.classList.add("dark"); } else { root.classList.remove("dark"); } }; updateResolvedTheme(); // Listen for system theme changes const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => { if (theme === "system") { updateResolvedTheme(); } }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme, resolvedTheme]); const handleSetTheme = (newTheme: Theme) => { setTheme(newTheme); localStorage.setItem("theme", newTheme); }; return ( <ThemeContext.Provider value={{ theme, setTheme: handleSetTheme, resolvedTheme }} > {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (context === undefined) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; }