Skip to main content
Frontend

Dark Mode That Does Not Flash or Break

9 min read

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

typescript

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:

javascript

Step 3 — Wrap cookie access in server functions

typescript

Step 4 — Update theme in the provider

When the user toggles, write the cookie and refresh server data:

typescript

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.