Dark Mode That Does Not Flash or Break
Themed apps on SSR frameworks have a timing problem. The server renders HTML before JavaScript runs. If the server and browser disagree on light vs dark, users see a flash — and React warns about hydration mismatches.
Vocabulary
| Term | Meaning |
|---|---|
| SSR | Server sends HTML with content already rendered |
| Hydration | React attaches to that HTML without re-drawing it |
| FOUC | Flash of wrong theme before JavaScript corrects it |
| Cookie | Sent with every HTTP request — readable on server and client |
Why localStorage fails
localStorage exists only in the browser. The server always renders the default (light). JavaScript then switches to dark. Mismatch. Flash. Console warnings.
Steps
Step 1 — Read theme from a cookie on the server
Pass this value into your root layout's theme provider.
Step 2 — Inline script before React loads
Runs in <head> before first paint. Reads the same cookie:
Step 3 — Wrap cookie access in server functions
Step 4 — Update theme in the provider
When the user toggles, write the cookie and refresh server data:
Step 5 — Define light and dark in CSS
:root holds light values. .dark overrides them. The inline script toggles the class on <html>.
Common pitfalls
- Three different sources of truth (localStorage, cookie, inline script) with different logic.
- Setting theme only in React state — server still renders default on refresh.
- Missing inline script — FOUC returns even with cookies.
Verify it works
Set dark mode. Hard-refresh. Background should be dark immediately. Console: no hydration warnings.
Takeaway
Cookie is the single source of truth. Server, inline script, and toggle must all read it with the same logic. That is the entire SSR dark-mode recipe.