Every dark mode implementation has the same enemy: the flash.
The page renders in light mode, then instantly switches to dark. It happens because JavaScript applies the CSS class after the HTML is already painted -- and by then it's too late.
Why It Happens
Browsers paint HTML before JavaScript runs. By the time your JS reads localStorage and adds dark to <html>, the first frame is already done.
The only real fix is a blocking inline script in <head> that runs before any rendering. That's what next-themes handles automatically.
The Key Pieces
1. suppressHydrationWarning goes on <html>, not <body>
Most guides get this wrong. Without it on the right element, you'll still see hydration warnings.
2. Tailwind v4 dropped darkMode: 'class'
Add this to globals.css instead:
@custom-variant dark (&:where(.dark, .dark *));
This is the #1 reason dark mode breaks after upgrading to Tailwind v4.
3. Mount-guard your toggle button
useTheme() returns undefined during hydration. Render null until mounted or your toggle will flicker.
Full setup with complete code, Cloudflare Pages notes, and troubleshooting:
👉 https://devencyclopedia.com/blog/nextjs-dark-mode-without-flash
Top comments (0)