2025-07-21 by Remi Kristelijn
When refreshing the page or navigating between routes, users experienced a brief flash of white background before the theme switched to their preferred dark mode. This is a common issue known as Flash of Unstyled Content (FOUC).
The flash occurs because:
localStorage, which isn't available during SSRThe most effective solution is using MUI's CSS theme variables approach, which prevents SSR flickering by using CSS variables instead of JavaScript-based theme switching.
Update src/lib/theme.ts to use MUI's CSS variables:
1import { createTheme } from '@mui/material/styles'; 2 3const theme = createTheme({ 4 cssVariables: { 5 colorSchemeSelector: 'class', // Use class-based theme switching 6 }, 7 colorSchemes: { 8 light: { 9 palette: { 10 primary: { 11 main: '#1976d2', 12 light: '#42a5f5', 13 dark: '#1565c0', 14 contrastText: '#ffffff', 15 }, 16 secondary: { 17 main: '#dc004e', 18 light: '#ff5983', 19 dark: '#9a0036', 20 contrastText: '#ffffff', 21 }, 22 background: { 23 default: '#fafafa', 24 paper: '#ffffff', 25 }, 26 text: { 27 primary: 'rgba(0, 0, 0, 0.87)', 28 secondary: 'rgba(0, 0, 0, 0.6)', 29 }, 30 }, 31 }, 32 dark: { 33 palette: { 34 primary: { 35 main: '#90caf9', 36 light: '#e3f2fd', 37 dark: '#42a5f5', 38 contrastText: '#000000', 39 }, 40 secondary: { 41 main: '#f48fb1', 42 light: '#f8bbd9', 43 dark: '#ec407a', 44 contrastText: '#000000', 45 }, 46 background: { 47 default: '#121212', 48 paper: '#1e1e1e', 49 }, 50 text: { 51 primary: '#ffffff', 52 secondary: 'rgba(255, 255, 255, 0.7)', 53 }, 54 }, 55 }, 56 }, 57 // ... typography and other theme options 58}); 59 60export default theme;
Update src/components/ThemeRegistry.tsx to use Experimental_CssVarsProvider:
1'use client'; 2 3import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; 4import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material/styles'; 5import CssBaseline from '@mui/material/CssBaseline'; 6import { useColorScheme } from '@mui/material/styles'; 7import { createContext, useContext, useEffect, ReactNode } from 'react'; 8import theme from '../lib/theme'; 9 10// Theme context for managing theme state 11const ThemeContext = createContext<{ 12 mode: 'light' | 'dark'; 13 toggleTheme: () => void; 14}>({ 15 mode: 'light', 16 toggleTheme: () => {}, 17}); 18 19export const useTheme = () => useContext(ThemeContext); 20 21// Theme provider component that manages theme state 22function ThemeProviderWrapper({ children }: { children: ReactNode }) { 23 const { mode, setMode } = useColorScheme(); 24 25 useEffect(() => { 26 // Load theme preference from localStorage 27 const savedMode = localStorage.getItem('theme-mode') as 'light' | 'dark'; 28 if (savedMode && (savedMode === 'light' || savedMode === 'dark')) { 29 setMode(savedMode); 30 } else { 31 // Check system preference 32 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 33 setMode(prefersDark ? 'dark' : 'light'); 34 } 35 }, [setMode]); 36 37 const toggleTheme = () => { 38 const newMode = mode === 'light' ? 'dark' : 'light'; 39 setMode(newMode); 40 localStorage.setItem('theme-mode', newMode); 41 }; 42 43 return ( 44 <ThemeContext.Provider value={{ mode: (mode === 'system' ? 'light' : mode) || 'light', toggleTheme }}> 45 {children} 46 </ThemeContext.Provider> 47 ); 48} 49 50export default function ThemeRegistry({ children }: { children: React.ReactNode }) { 51 return ( 52 <AppRouterCacheProvider> 53 <CssVarsProvider theme={theme} defaultColorScheme="light"> 54 <ThemeProviderWrapper> 55 <CssBaseline /> 56 {children} 57 </ThemeProviderWrapper> 58 </CssVarsProvider> 59 </AppRouterCacheProvider> 60 ); 61}
Add a script in src/app/layout.tsx to detect and apply the theme before any content renders:
1// src/app/layout.tsx 2<html lang="en" suppressHydrationWarning> 3 <head> 4 <meta name="emotion-insertion-point" content="" /> 5 <script 6 dangerouslySetInnerHTML={{ 7 __html: ` 8 (function() { 9 try { 10 var mode = localStorage.getItem('theme-mode'); 11 if (!mode) { 12 var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 13 mode = prefersDark ? 'dark' : 'light'; 14 } 15 document.documentElement.classList.add('mui-' + mode); 16 } catch (e) { 17 document.documentElement.classList.add('mui-light'); 18 } 19 })(); 20 `, 21 }} 22 /> 23 </head> 24 <body> 25 {/* ... rest of layout */} 26 </body> 27</html>
Important: The suppressHydrationWarning attribute prevents hydration errors when the script adds theme classes to the <html> element.
Create src/app/globals.css with CSS variables support:
1/* Global styles for MUI CSS theme variables */ 2 3/* Initial theme setup - prevents flash of unstyled content */ 4html.mui-light { 5 color-scheme: light; 6} 7 8html.mui-dark { 9 color-scheme: dark; 10} 11 12/* Set initial background colors to prevent flash */ 13body { 14 background-color: #fafafa; /* Light theme default */ 15 transition: background-color 0.2s ease; 16} 17 18html.mui-dark body { 19 background-color: #121212; /* Dark theme default */ 20} 21 22/* MUI CSS Variables fallbacks */ 23:root { 24 --mui-palette-primary-main: #1976d2; 25 --mui-palette-background-default: #fafafa; 26 --mui-palette-background-paper: #ffffff; 27 --mui-palette-text-primary: rgba(0, 0, 0, 0.87); 28 /* ... other light theme variables */ 29} 30 31html.mui-dark { 32 --mui-palette-primary-main: #90caf9; 33 --mui-palette-background-default: #121212; 34 --mui-palette-background-paper: #1e1e1e; 35 --mui-palette-text-primary: #ffffff; 36 /* ... other dark theme variables */ 37}
theme.palette.mode === 'dark' conditions needed<head> before any content rendersmui-light or mui-dark) to <html> elementsuppressHydrationWarning prevents errors when theme classes are added1// ❌ Old approach with manual theme checking 2<Box sx={{ 3 backgroundColor: theme.palette.mode === 'dark' ? '#1e1e1e' : '#ffffff', 4 color: theme.palette.mode === 'dark' ? '#ffffff' : '#000000' 5}}>
1// ✅ New approach with CSS variables 2<Box sx={{ 3 backgroundColor: 'background.paper', 4 color: 'text.primary' 5}}>
background.paper, text.primarytheme.palette.mode === 'dark' conditionssuppressHydrationWarning to prevent hydration errorsBy implementing MUI's CSS theme variables approach, we've completely eliminated the FOUC issue while simplifying the codebase. The solution provides:
The key insight is that CSS variables provide immediate theme application without requiring JavaScript execution, making them the ideal solution for preventing SSR flickering in Next.js applications with Material-UI.