Community JavaScript Snippet
The Theme-Switcher Context I Drop Into Every App
The drop-in `<ThemeProvider>` I keep in my dotfiles. CSS variables, `prefers-color-scheme` integration, and `localStorage` persistence in about 60 lines.
The Theme-Switcher Context I Drop Into Every App
The drop-in `<ThemeProvider>` I keep in my dotfiles. CSS variables, `prefers-color-scheme` integration, and `localStorage` persistence in about 60 lines.
By @kiranpatel
November 30, 2025
·
Updated May 18, 2026
654 views
10
4.3 (13)
I keep the token table in a plain object because I want any colour change to be a one-line edit, not a styled-components migration. The useEffect writes the active set onto documentElement so children read var(--bg) directly without re-rendering on theme flips, which is the whole performance reason to do this with CSS variables instead of context-driven inline styles. Wrapping the value in useMemo keyed on theme is the same idea as the dedicated context-memoization snippet; without it every parent re-render would refresh consumers. The default fallback in createContext matters because it is what useTheme() returns when a consumer is rendered outside the provider, and silently broken theming is worse than a thrown error in development.
Three subtle choices live in this hook. First, the initial-theme function is passed lazily to useState so we touch localStorage exactly once per mount, never on re-renders. Second, the change listener on matchMedia only updates state when the user has not pinned a preference, otherwise the OS toggle would silently overwrite a deliberate choice. Third, I never persist on first render: the effect only runs after a setTheme, so a fresh visit that gets 'dark' from the OS hint does not write 'dark' into storage and accidentally pin it. The shims at the top exist so the snippet still runs in this playground; in a real browser bundle you delete those eight lines.
The whole reason for the context is that the toggle component does not need to know how persistence works, only { theme, toggle }. I write the toggle as a single line in real apps; the bulk of this accordion is the playground harness. aria-label is the only accessibility hook I never skip on icon-only buttons because dark-mode toggles are nine times out of ten just a sun-or-moon glyph. Note that the official react-uselocalstorage-hook and react-usemediaquery-hook snippets each do half of this on their own; this entry composes them into the actual provider I ship.
