Dark mode is three problems pretending to be one: which mode is active, where the choice lives, and how the page renders before React mounts. Solve all three or your toggle flickers on every reload.
The three layers
- The current mode — light, dark, or 'follow system.'
- Persistence — the user's choice should survive page reloads (localStorage).
- Initial render — the HTML must already have the right theme attribute before any React renders, or the page flashes the wrong colors for a frame.
The pre-React script
To avoid the flash, set the theme attribute on <html> in an inline script in index.html — before Vite's bundle even loads. Read from localStorage; fall back to prefers-color-scheme. This script runs synchronously and the page paints with the right colors immediately.
The React hook
Inside React, a useTheme hook reads the current attribute, exposes a setter, and writes back to localStorage. The hook drives any in-page toggle UI. The pre-React script handled the boot; the hook handles user-driven changes.
'Follow system'
Some users want the OS preference. Listen for matchMedia('(prefers-color-scheme: dark)') change events. If the user hasn't picked an explicit mode (no localStorage entry), follow the system signal. The moment they pick a mode, override the system signal.