Tailwind CSS in Production · Theming and Consistency

The White Flash in Dark Mode

Users who chose dark mode saw a bright white flash on every page load, because the dark class was added by JavaScript after the first paint.

Create a free account to save this page, take notes and track your progress. Sign in

Scenario

Example team: an app adds a theme toggle using class-based dark mode. A React effect reads the saved choice from localStorage and adds dark to <html> after the app mounts.

What you see in production

Every navigation in dark mode starts with a white page for a fraction of a second; on slow phones it lasts long enough to be painful at night. Users report "the flash".

The naive code

useEffect(() => {
  if (localStorage.theme === "dark") document.documentElement.classList.add("dark");
}, []);

Reproduce it

The order of events decides what the user sees first:

When does the dark class arrive?Try it yourself
def timeline(setup):
    events = []
    dark = False
    for step in ["html parsed", "css loaded", "first paint", "js bundle runs", "react mounts", "effect runs"]:
        if setup == "inline head script" and step == "html parsed":
            dark = True                      # a tiny script in <head> runs before any paint
        if setup == "useEffect" and step == "effect runs":
            dark = True
        if step == "first paint" or step == "effect runs":
            events.append(f"{step}: {'dark' if dark else 'WHITE'}")
    return events

for setup in ("useEffect", "inline head script"):
    print(f"{setup:20}", " -> ".join(timeline(setup)))

Output

useEffect            first paint: WHITE -> effect runs: dark
inline head script   first paint: dark -> effect runs: dark

Root cause

The browser paints as soon as HTML and CSS are ready. Code in your bundle, and especially in effects after hydration, runs after the first paint, so the page renders light first.

The fix

Set the class before the first paint with a tiny blocking script in <head>, and keep the toggle code for later changes:

<script>
  document.documentElement.classList.toggle(
    "dark",
    localStorage.theme === "dark" ||
      (!("theme" in localStorage) && matchMedia("(prefers-color-scheme: dark)").matches),
  );
</script>

Also add <meta name="color-scheme" content="light dark"> (or color-scheme in CSS) so form controls and scrollbars match. Server-rendered apps can render the class from a cookie instead.

Class-based dark mode, both themesTry it yourself
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>
<div class="grid sm:grid-cols-2">
  <div class="bg-white p-6 text-gray-900 dark:bg-gray-950 dark:text-gray-100">Light page</div>
  <div class="dark bg-white p-6 text-gray-900 dark:bg-gray-950 dark:text-gray-100">Dark page, same classes</div>
</div>

Trade-offs

  • An inline script must be allowed by your Content-Security-Policy (use a nonce or hash).
  • Following the system setting with the default media-query strategy needs no script at all, but gives users no choice.

Detect it in production

  • Record a page load in dark mode with CPU throttling; any light frame is a flash.
  • Check that the theme script is the first script in <head>.

Key points

  • Anything after first paint is too late for the theme class.
  • Use an inline head script (or server rendering) to set it.

Practice questions

How do you prevent the flash of the light theme with class-based dark mode?

Answer

Set the dark class on <html> before first paint: a small inline script in <head> reads the saved preference (or the system setting) and toggles the class; or render it server-side from a cookie. Effects in the app bundle run too late.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?