sayeed.net
← back
2 min read

Dark Mode Without the Flash

#css#nextjs#ux

Listen

The classic dark mode flash problem: your page loads, briefly shows the wrong theme, then snaps to the correct one. I stuggled with this issue while writing this blog and eventaully figured out the solution.

Why the flash happens

React hydration is asynchronous. By the time your theme provider reads localStorage and sets the theme, the browser has already painted the default styles. The user sees a flash.

The simple fix: a blocking inline script

Add a small script directly to your <head> — before any stylesheets or React code runs:

<script
  dangerouslySetInnerHTML={{
    __html: `
      try {
        var t = localStorage.getItem('theme');
        if (!t) t = window.matchMedia('(prefers-color-scheme: dark)').matches
          ? 'dark' : 'light';
        document.documentElement.setAttribute('data-theme', t);
      } catch(e) {}
    `,
  }}
/>

This runs synchronously, before paint, so the correct theme is applied before the browser renders anything visible.

CSS variables approach

Store your theme values in CSS custom properties on the :root:

:root {
  --bg: #f5f5f0;
  --text: #1a1a1a;
}

[data-theme="dark"] {
  --bg: #0d0d0f;
  --text: #e8e8e0;
}

Switching themes is then a single setAttribute call — no class toggling, no CSS-in-JS overhead.

The result

Zero flash. The browser reads the data-theme attribute set by the inline script, resolves the CSS variables, and paints correctly on the very first frame.

← all posts