3 min read

Fixing Dark Mode Flickering (FOUC) in React and Next.js

react
dark-mode
fouc
tailwind
nextjs

Flickering in React or Next.js dark mode—often referred to as a Flash of Unstyled Content (FOUC)—is a common issue where the page briefly displays light mode styles before switching to dark mode.

This flicker can be jarring and negatively impact the user experience, especially on slower networks or devices.

💡 What Causes Dark Mode Flickering?

1. Server-Side Rendering (SSR)

When using SSR (e.g., with Next.js), the server doesn't know the user's preferred color scheme since it's not available in the document body which is only available in the window object that is not available in the server. As a result, the initial HTML renders with default (usually light) styles, and dark mode is only applied after hydration.

2. Slow JavaScript Loading

If the JavaScript responsible for setting the theme takes too long to execute, users will see the default light theme before it switches to dark mode. This is because the JavaScript is not executed until the DOM is fully loaded and the React app is hydrated.

3. FOUC (Flash of Unstyled Content)

This visual glitch occurs when the page briefly renders with default styles before the dark mode styles are applied, causing a flicker.


✅ How to Prevent Flickering

Let's see how we can fix the flickering issue in React and Next.js. So we can provide a smooth transition between light, dark, and system themes. And better user experience.

1. Apply Styles in the <head>

Add an inline script or a class to the HTML that immediately applies the dark theme before the React app loads.

Example (Vite with index.html)

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <script>
    try {
      // Get the theme from localStorage
      const theme = localStorage.getItem('theme')
      // Get the system theme
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
      // Get the effective theme
      const effectiveTheme = theme === 'system' ? systemTheme : theme
      // Add the theme to the document
      document.documentElement.classList.add(effectiveTheme)
    } catch (error) {
      console.error(error)
    }
  </script>
 
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Example (Next.js with layout.tsx)

Or if you are using Next.js, you can use the the next <script> tag.

tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              try {
                const theme = localStorage.getItem("theme");
                const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
                  .matches
                  ? "dark"
                  : "light";
                const effectiveTheme = theme === "system" ? systemTheme : theme;
                document.documentElement.classList.add(effectiveTheme);
              } catch (error) {
                console.error(error);
              }
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

2. Use next-themes

You can use the next-themes library for your react or nextjs project and it works fine.

Install the next-themes package

pnpm

bash
pnpm install next-themes

npm

bash
npm install next-themes

yarn

bash
yarn add next-themes

Create a theme provider

components/theme-provider.tsx
// components/theme-provider.tsx
'use client'
 
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
 
export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Wrap your root layout

layout.tsx
// layout.tsx
import { ThemeProvider } from '@/components/theme-provider'
 
export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head />
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Add a mode toggle

components/mode-toggle.tsx
// components/mode-toggle.tsx
import { useTheme } from 'next-themes'
 
export default function ModeToggle() {
  const { theme, setTheme } = useTheme()
 
  return (
    <div className="flex h-screen flex-col items-center justify-center gap-4">
      <h1 className="text-6xl font-bold">Hello World</h1>
 
      <h2 className="rounded-md border border-gray-300 p-2 text-2xl font-bold">
        Current Theme: {theme}
      </h2>
 
      <button
        className="rounded-md bg-blue-500 p-2 text-white"
        onClick={() => setTheme('system')}
      >
        Toggle Theme (system)
      </button>
 
      <button
        className="rounded-md bg-blue-500 p-2 text-white"
        onClick={() => setTheme('dark')}
      >
        Toggle Theme (dark)
      </button>
 
      <button
        className="rounded-md bg-blue-500 p-2 text-white"
        onClick={() => setTheme('light')}
      >
        Toggle Theme (light)
      </button>
    </div>
  )
}

And that's it! 🎉

This will prevent the flickering issue and provide a smooth transition between light, dark, and system themes.

📌 Example

I have also prepared the Example of both React and Next.js. So checking it out will be helpful.

Thanks for reading! ✨