Why Is My Dark Mode Toggle Switch Flickering On Page Reload?
You set up a beautiful dark mode toggle. It works perfectly on click. But then you refresh the page, and for a split second, you see a bright white flash before your dark theme snaps back into place.
That tiny blink feels jarring. It makes your polished site look broken. If this sounds familiar, you are in the right place.
This flicker has a name. Developers call it the Flash of Unstyled Content or FOUC. Some call it FART, which stands for Flash of inAccurate coloR Theme.
In a Nutshell:
- The flicker happens because of timing. Your browser paints the default theme first, then your JavaScript runs and switches the theme. That gap between paint and switch is the flash you see.
- The single best fix is a render blocking script in the
<head>. A tiny inline script that readslocalStorageand sets the theme before the body renders stops the flash completely. - Never put your theme logic at the bottom of the page or inside a deferred script. Late running code always paints the wrong color first.
- Use the
color-schemeCSS property to help the browser style form controls, scrollbars, and the page background correctly during the first paint. - Framework users need framework specific tools. In Next.js, the
next-themeslibrary withsuppressHydrationWarningsolves both the flicker and hydration mismatch. - Store the theme in
localStorage, not just in memory. A toggle that forgets the choice on reload will always flicker back to default.
What Causes The Dark Mode Flicker On Page Reload?
The flicker comes from a race condition between rendering and scripting. When a browser loads your page, it parses the HTML and paints it on screen as fast as possible. It does not wait for every script to finish.
If your theme switching code runs late, the browser has already drawn the page using its default styles, usually light mode. Then your JavaScript reads the saved preference and flips the theme. That correction is the flash you see.
The root issue is simple: the browser shows one theme, then swaps to another. Your eyes catch that swap as a blink. The fix always involves making sure the correct theme is set before the first paint, not after it. Once you grasp this timing problem, every solution below will make sense.
Why Your Theme Setting Loads Too Late
Most flicker problems trace back to where your script sits in the document. If you place your theme logic at the end of the <body> or inside a file loaded with defer or async, it runs after the browser has already painted the screen.
The same thing happens in single page apps. React, Vue, and similar frameworks hydrate the page after the initial HTML loads. During that delay, the default theme shows.
The timing chain looks like this: the HTML loads, the browser paints light mode, the bundle downloads, your component mounts, and only then does dark mode apply.
Each step adds milliseconds. Those milliseconds are exactly the flash. To stop it, you must move the decision earlier in the load process. The earlier the theme is set, the smaller the gap becomes, until it disappears entirely.
The Best Fix: Add A Render Blocking Script In The Head
This is the most reliable solution for almost every site. You place a small inline script inside the <head> of your HTML, before any visible content. This script reads the saved theme and applies a class to the <html> element before the browser paints anything.
Here is a clean example you can paste into your <head>:
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
})();
</script>
Why this works so well: the script is render blocking, which means the browser pauses painting until it finishes. By the time the page draws, the correct class is already in place. No swap, no flash.
Pros: It works on any site, framework or not. It is tiny and fast. It needs no dependencies.
Cons: Inline scripts can clash with a strict Content Security Policy. You may need to add a nonce or hash to allow it.
How To Set Up Your CSS For A Flash Free Theme
Your script sets a class, but your CSS must respond to that class instantly. The cleanest method uses CSS variables on the :root element. You define light colors by default and override them when the dark class is present.
Here is the structure:
:root {
--bg: #ffffff;
--text: #111111;
}
:root.dark {
--bg: #111111;
--text: #ffffff;
}
body {
background: var(--bg);
color: var(--text);
}
The benefit of variables is consistency. You change two values and the whole page updates. There is no need to rewrite styles for every element.
Pros: Variables keep your code clean and easy to maintain. Switching themes becomes one class toggle.
Cons: Very old browsers do not support CSS variables, though this rarely matters today. You also need discipline to route every color through a variable, or some elements will ignore the theme.
Using The CSS color-scheme Property To Stop Background Flash
Sometimes the flash is not your content but the browser’s own default background, scrollbars, and form controls. The color-scheme property tells the browser which native styles to use during the very first paint.
Add this to your CSS or set it with your inline script:
:root {
color-scheme: light dark;
}
This single line helps the browser paint a sensible background color before your styles load. It also styles checkboxes, dropdowns, and scrollbars to match the theme automatically.
For a saved dark preference, you can set document.documentElement.style.colorScheme = 'dark' inside your head script. This makes the browser background dark from the first moment.
Pros: It fixes native UI elements you cannot easily style otherwise. It is one line and widely supported.
Cons: It does not replace your full theme logic. You still need the head script for your own colors. It also offers limited control over exact shades.
Why localStorage Is The Key To Remembering The Theme
A toggle that flickers on reload often forgets the user’s choice. If you store the theme only in a React state or a memory variable, it resets every time the page loads. The default theme then shows again.
localStorage solves this. It saves the preference in the browser permanently, so it survives reloads and even browser restarts.
When the user clicks the toggle, save the value:
localStorage.setItem('theme', isDark ? 'dark' : 'light');
Then your head script reads that value on the next load. The two pieces work together: one saves, the other restores.
Pros: It is simple, fast, and built into every browser. It persists without a server or cookies.
Cons: localStorage is not available during server side rendering, which causes problems in Next.js. We cover that fix below. It also fails if a user blocks storage, so plan a sensible default.
How To Fix The Flicker In React Apps
React apps flicker because components mount after the initial HTML paints. Your toggle component reads localStorage only once it loads, which is too late.
The trick is to split the work. Put the theme reading logic in a blocking head script in your public/index.html, exactly like the plain HTML fix above. Let React handle only the toggle button and the state after that.
Do not rely on useEffect to set the initial theme. Effects run after paint, so they always cause a flash. Use them only to sync changes when the user clicks.
A simple pattern is to read the class already set by your head script:
const [isDark, setIsDark] = useState(
() => document.documentElement.classList.contains('dark')
);
Pros: It keeps React clean and the load flash free. The head script does the heavy lifting.
Cons: You manage two places: the static HTML and the component. This split can feel awkward at first but works reliably.
How To Stop The Flash In Next.js And Server Rendered Sites
Next.js is trickier because pages render on the server, where localStorage does not exist. The server cannot know the user’s theme, so it sends light mode HTML. The browser then corrects it, causing both a flash and a hydration mismatch warning.
The cleanest solution is the next-themes library. It injects a blocking script automatically and handles all the edge cases for you.
import { ThemeProvider } from 'next-themes';
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
You must add suppressHydrationWarning to your <html> tag. This tells React to ignore the expected mismatch on that element, since the theme class differs between server and client.
<html lang="en" suppressHydrationWarning>
Pros: It handles system preference, persistence, and flicker in a few lines. It is battle tested.
Cons: It adds a dependency. You still need suppressHydrationWarning, or you will see console warnings.
How To Handle System Preference With prefers-color-scheme
Many users want your site to match their device setting automatically. The prefers-color-scheme media query detects whether the operating system uses light or dark mode.
You can read it in CSS:
@media (prefers-color-scheme: dark) {
:root {
--bg: #111111;
--text: #ffffff;
}
}
But there is a catch. If you mix system preference with a saved manual choice, your head script must decide which wins. Usually the saved choice should override the system setting.
Your script logic becomes: check localStorage first, and if nothing is saved, fall back to the system preference using window.matchMedia('(prefers-color-scheme: dark)').matches.
Pros: It respects the user’s device and feels natural. It needs no extra clicks.
Cons: The logic gets more complex when you allow manual overrides. You must test all four combinations of saved and system states to avoid surprises.
A Complete Head Script That Handles Everything
Now let us combine every idea into one script that handles saved preferences, system fallback, and the color scheme. Paste this into your <head> before any visible content.
<script>
(function() {
const saved = localStorage.getItem('theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches;
const dark = saved === 'dark' || (!saved && system);
if (dark) {
document.documentElement.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
}
})();
</script>
This script does three jobs at once. It checks the saved choice, falls back to the system setting, and sets both the class and the native color scheme. All of this happens before the first paint.
Pros: It is complete, fast, and framework agnostic. It covers nearly every real situation.
Cons: Inline scripts may need a CSP nonce. Keep the script short so it does not slow your first paint noticeably.
How To Test If Your Fix Actually Works
Fixing the flicker is one thing. Confirming it is gone is another. The flash is fast, so your eyes may miss it. Use proper tools to be sure.
Open your browser developer tools and use the network throttling option. Set it to a slow speed like “Slow 3G.” This stretches the load time and exaggerates any flash, making it easy to spot.
You can also record the page load. In Chrome, the Performance tab lets you capture a load and step through frames one by one. Look at the first painted frame and check its color.
Do a hard reload with the cache cleared, since cached files load too fast to show problems. Test in dark mode, light mode, and with no saved preference.
Pros: Throttling reveals flashes you cannot see at full speed. Frame by frame review is precise.
Cons: It takes a few extra minutes. Results vary by device, so test on both desktop and mobile.
Common Mistakes That Bring The Flicker Back
Even after a good fix, small habits reintroduce the flash. Watch for these traps. The most common one is moving the theme script out of the <head> to “clean up” the HTML. That single move undoes everything.
Another mistake is adding defer or async to the theme script. Both delay execution past the first paint, so the flash returns. Your theme script must run synchronously and early.
People also forget to set the class on the <html> element and use <body> instead. The <html> element paints first, so target it for the earliest effect.
Finally, some developers transition the colors with CSS animations that run on load. Disable transitions during the initial load and enable them only after the page settles, or the theme will appear to fade in.
FAQs
Why does my dark mode flash white even though I saved the theme?
The save is working, but your script reads it too late. The browser paints the default light theme before your code runs. Move your theme reading script into the <head> as a synchronous inline script so it runs before the first paint.
Does a render blocking script slow down my website?
A tiny inline script of a few lines adds almost no measurable delay. It runs in under a millisecond. The trade off is worth it, since it removes the flash entirely. Just keep the script short and avoid heavy logic inside it.
Why do I see a hydration warning in Next.js when fixing dark mode?
The server renders light mode because it cannot read localStorage, but the client applies the saved theme. This difference triggers the warning. Add suppressHydrationWarning to your <html> tag, or use the next-themes library which handles this for you.
Should I use cookies instead of localStorage for the theme?
For server rendered sites, cookies help because the server can read them and send the correct theme in the HTML. This removes the flash without client side scripts. For plain static or client side sites, localStorage is simpler and works perfectly.
Why does the flicker only happen on the first visit?
On the first visit, no preference is saved yet, so your script may fall back to the default before deciding. Make sure your head script checks the system preference with prefers-color-scheme as an immediate fallback, so even new visitors get the right theme instantly.
Can CSS alone fix the dark mode flicker without JavaScript?
If you rely only on prefers-color-scheme, then yes, CSS alone matches the system theme with no flash. But the moment you add a manual toggle that overrides the system, you need JavaScript and localStorage, and then the head script becomes necessary.

