Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions components/shared/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { createContext, useContext, useEffect, useState, useCallback } from "react";
import { createContext, useContext, useCallback, useSyncExternalStore } from "react";

type Theme = "dark" | "light";
interface ThemeCtx {
Expand All @@ -11,33 +11,46 @@ interface ThemeCtx {

const Ctx = createContext<ThemeCtx | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
// Seed from the attribute the pre-paint inline script already set (in
// app/layout.tsx), so React does not re-flip the theme after hydration.
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof document !== "undefined") {
const t = document.documentElement.dataset.theme;
if (t === "dark" || t === "light") return t;
}
return "dark";
function getServerSnapshot(): Theme {
return "dark";
}

function subscribe(callback: () => void): () => void {
const obs = new MutationObserver(callback);
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
return () => obs.disconnect();
}

function getClientSnapshot(): Theme {
const t = document.documentElement.dataset.theme;
return t === "light" ? "light" : "dark";
}

function applyTheme(t: Theme) {
const d = document.documentElement;
d.setAttribute("data-theme", t);
d.style.colorScheme = t;
try {
window.localStorage.setItem("cc:theme", t);
} catch {
/* ignore */
}
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const theme = useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);

const setTheme = useCallback((t: Theme) => applyTheme(t), []);
const toggle = useCallback(() => applyTheme(getClientSnapshot() === "dark" ? "light" : "dark"), []);

// Reflect on <html data-theme> and keep the native color-scheme in sync.
useEffect(() => {
const d = document.documentElement;
d.setAttribute("data-theme", theme);
d.style.colorScheme = theme;
try {
window.localStorage.setItem("cc:theme", theme);
} catch {
/* ignore */
}
}, [theme]);

const setTheme = useCallback((t: Theme) => setThemeState(t), []);
const toggle = useCallback(() => setThemeState((t) => (t === "dark" ? "light" : "dark")), []);

return <Ctx.Provider value={{ theme, setTheme, toggle }}>{children}</Ctx.Provider>;
return (
<Ctx.Provider value={{ theme, setTheme, toggle }}>
{children}
</Ctx.Provider>
);
}

export function useTheme(): ThemeCtx {
Expand Down