[MUI v6] Setting default mode flickers the light/dark theme at first render
Steps to reproduce
Link to the initial issue: https://github.com/mui/material-ui/issues/43622
Steps:
- Set your system (OS) default theme to dark
- Set the default MUI theme mode to
light - Open the page in incognito mode
- The page first renders with the
darkmode and then changes tolight, causing flickering
Make sure to re-open the page in incognito mode or change the modeStorageKey. Otherwise, the key will be stored and you won't notice any flickering.
Here is a deployed example: https://profound.academy
Current behavior
If your system default mode is dark, and you set your website default theme to be light, the very first render is dark, which then switches to light, causing flickering. That's pretty unpleasant for the visitors.
Expected behavior
It should render using the defaultMode right away without any flickering.
Context
No response
Your environment
npx @mui/envinfo
Using Chrome
System:
OS: macOS 14.6.1
Binaries:
Node: 20.10.0 - /opt/homebrew/opt/node@20/bin/node
npm: 10.2.3 - /opt/homebrew/opt/node@20/bin/npm
pnpm: Not Found
Browsers:
Chrome: 129.0.6668.71
Edge: Not Found
Safari: 17.6
npmPackages:
@emotion/react: ^11.13.3 => 11.13.3
@emotion/styled: ^11.13.0 => 11.13.0
@mui/core-downloads-tracker: 6.1.2
@mui/icons-material: ^6.1.2 => 6.1.2
@mui/material: ^6.1.2 => 6.1.2
@mui/private-theming: 6.1.2
@mui/styled-engine: 6.1.2
@mui/system: 6.1.2
@mui/types: 7.2.17
@mui/utils: 5.16.6
@mui/x-charts: ^7.18.0 => 7.18.0
@mui/x-charts-vendor: 7.18.0
@mui/x-data-grid: ^7.18.0 => 7.18.0
@mui/x-date-pickers: ^7.18.0 => 7.18.0
@mui/x-internals: 7.18.0
@mui/x-tree-view: ^7.18.0 => 7.18.0
@types/react: ^18.3.10 => 18.3.10
react: ^18.3.1 => 18.3.1
react-dom: ^18.3.1 => 18.3.1
styled-components: 5.3.11
typescript: ^5.6.2 => 5.6.2
Search keywords: default mode, flickering, MUI v6
@aarongarciah do you know why this might be happening?
Can you provide a code sample that you are using for the ThemeProvider? I tested locally with a fresh Next.js project and it works fine.
// theme.js
const theme = createTheme({
cssVariables: {
colorSchemeSelector: "class",
},
colorSchemes: {
light: true,
dark: true,
},
typography: {
fontFamily: roboto.style.fontFamily,
},
});
// layout.tsx
export default function RootLayout(props: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider defaultMode="light" theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{props.children}
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
Sure, I think you're missing the InitColorSchemeScript. If I remove it the flickering goes away on the first render, but the whole app flickers on all the next renders if I switch to dark mode and refresh the page.
Here is the ThemeProvider.tsx:
'use client';
import {memo, ReactNode} from "react";
import {Roboto} from "next/font/google";
import {createTheme, ThemeProvider as MuiThemeProvider} from "@mui/material/styles";
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
import CssBaseline from "@mui/material/CssBaseline";
import ColorModeContextProvider from "@/theme/ColorModeContext";
import {AppRouterCacheProvider} from "@mui/material-nextjs/v14-appRouter";
const font = Roboto({weight: ['300', '400', '500', '700'], subsets: ['latin']});
export const theme = createTheme({
cssVariables: {colorSchemeSelector: 'class'},
defaultColorScheme: 'light',
typography: {
fontFamily: font.style.fontFamily,
h1: {fontSize: 42, fontWeight: 'bold'},
h2: {fontSize: 30},
h3: {fontSize: 26},
h4: {fontSize: 20},
h5: {fontSize: 18},
h6: {fontSize: 16},
},
colorSchemes: {
light: {
palette: {
background: {default: '#ffffff', paper: '#ffffff'},
primary: {main: '#212b36'},
secondary: {main: '#fa541c'},
info: {main: '#f44336'},
success: {main: '#4caf50'},
error: {main: '#f44336'},
// @ts-ignore
console: {main: '#e0e0e0'},
secondaryAction: {main: '#000000'},
neutral: {main: '#ffffff', contrastText: '#212121'},
unavailable: {main: '#363636'},
},
},
dark: {
palette: {
background: {default: '#171717', paper: '#212121'},
primary: {main: '#ffffff'},
secondary: {main: '#fa541c'},
info: {main: '#f44336'},
success: {main: '#388e3c'},
error: {main: '#d32f2f'},
// @ts-ignore
console: {main: '#212121'},
secondaryAction: {main: '#fff'},
neutral: {main: '#212121', contrastText: '#fff'},
unavailable: {main: '#797979'},
},
},
},
});
function ThemeProvider({children}: {children: ReactNode}) {
return <>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<InitColorSchemeScript modeStorageKey="theme-mode" attribute="class" />
<MuiThemeProvider modeStorageKey="theme-mode" defaultMode="light" theme={theme}>
<ColorModeContextProvider>
<CssBaseline />
{children}
</ColorModeContextProvider>
</MuiThemeProvider>
</AppRouterCacheProvider>
</>
}
export default memo(ThemeProvider);
And here is the ColorModeContext.tsx:
import {createContext, memo, ReactNode} from "react";
import {useColorScheme} from "@mui/material/styles";
export type PaletteMode = 'light' | 'dark' | 'system';
interface ColorModeContextProps {
mode: PaletteMode;
setMode: (mode: PaletteMode) => void;
}
export const ColorModeContext = createContext<ColorModeContextProps>({
mode: 'light',
setMode: (_: PaletteMode) => {},
});
function ColorModeContextProvider({children}: {children: ReactNode}) {
const {mode, setMode} = useColorScheme();
return <ColorModeContext.Provider value={{mode: mode ?? 'light', setMode: setMode}}>
{children}
</ColorModeContext.Provider>
}
export default memo(ColorModeContextProvider);
When I open the app in incognito mode (fresh window every time), it opens up in dark mode and stays in dark mode for 1-2 seconds. After which, it switches to light mode.
Sure, I think you're missing the
InitColorSchemeScript. If I remove it the flickering goes away on the first render, but the whole app flickers on all the next renders if I switch todarkmode and refresh the page.
Gotcha, you are right. The problem is the InitColorSchemeScript. At the beginning, the default mode is system (no local storage yet).
I will open a PR to fix this. You will need to provide the defaultMode to InitColorSchemeScript in the future too.
Sounds good, I think it would be pretty straightforward from the users' perspective (I assume that in most projects ThemeProvider and InitColorSchemeScript are located in the same file, so keeping them in sync shouldn't be a problem).
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue. Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.
[!NOTE] @MartinXPN How did we do? Your experience with our support team matters to us. If you have a moment, please share your thoughts in this short Support Satisfaction survey.