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
useEffectonly — a flash of light theme on load. - Unguarded
localStorageaccess (throws in some privacy modes). - Hard-coded colors that ignore the tokens.
Variations
- Next.js:
next-themeshandles 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
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Poll a long-running job's status
- Protect routes in a React SPA