React cookbook · Beginner

Implement dark mode that follows the system

Support light/dark themes with a manual override, persisted, and without a flash of the wrong theme.

Approach

Use CSS custom properties per theme, set data-theme on <html>, default to prefers-color-scheme, store the user's override in localStorage, and run a tiny inline script in index.html before React loads to apply the theme without a flash.

Solution

import { useEffect, useState } from "react";

type Theme = "light" | "dark" | "system";

export function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    try { return (localStorage.getItem("theme") as Theme) || "system"; } catch { return "system"; }
  });

  useEffect(() => {
    const media = window.matchMedia("(prefers-color-scheme: dark)");
    const apply = () => {
      const resolved = theme === "system" ? (media.matches ? "dark" : "light") : theme;
      document.documentElement.dataset.theme = resolved;
    };
    apply();
    try { localStorage.setItem("theme", theme); } catch { /* storage unavailable */ }
    media.addEventListener("change", apply);
    return () => media.removeEventListener("change", apply);
  }, [theme]);

  return [theme, setTheme] as const;
}
<!-- index.html, before the app bundle -->
<script>
  try {
    var t = localStorage.getItem("theme") || "system";
    if (t === "system") t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
    document.documentElement.dataset.theme = t;
  } catch (e) {}
</script>
:root { --bg: #ffffff; --text: #111827; color-scheme: light; }
:root[data-theme="dark"] { --bg: #0f172a; --text: #e5e7eb; color-scheme: dark; }
body { background: var(--bg); color: var(--text); }

Pitfalls

  • Theme applied in useEffect only — a flash of light theme on load.
  • Unguarded localStorage access (throws in some privacy modes).
  • Hard-coded colors that ignore the tokens.

Variations

  • Next.js: next-themes handles SSR and the no-flash script.
  • Offer a three-way toggle (Light / Dark / System).

Further reading: MDN: prefers-color-scheme

Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme

More React recipes