How to Implement Dark Mode in Material-UI Using InitColorSchemeScript
InitColorSchemeScript injects a blocking script into the HTML <head> that detects system preferences and reads localStorage before React hydrates, eliminating flash-of-unstyled-content (FOUC) when implementing dark mode in Material-UI.
The mui/material-ui repository provides a robust solution for theme switching through the InitColorSchemeScript component. This utility ensures your application respects user color scheme preferences immediately on page load, preventing visual inconsistencies during hydration. Learning how to implement dark mode in Material-UI using InitColorSchemeScript is essential for creating accessible, performant React applications that respond to OS-level settings.
What Is InitColorSchemeScript?
InitColorSchemeScript is a React component that renders a synchronous script tag designed to execute before your application paints. As implemented in packages/mui-material/src/InitColorSchemeScript/InitColorSchemeScript.js, this script performs four critical operations:
- Detects system preferences using the browser's
prefers-color-schememedia query - Reads persisted user selections from
localStorageunder the keymui-color-scheme - Sets global indicators by writing to the CSS custom property
--mui-color-schemeand the HTML element'sdata-mui-color-schemeattribute - Blocks rendering until execution completes, ensuring the initial paint matches the final theme
Because this script runs synchronously in the <head>, it prevents the flash of wrong color scheme (FOUC) that typically occurs when theme detection happens after React hydration.
Why Use InitColorSchemeScript for Dark Mode?
Implementing dark mode without this utility often results in a visible flash of light theme while JavaScript calculates the correct palette. The InitColorSchemeScript approach offers specific advantages:
- Zero runtime overhead after hydration – The script executes once during page load, then React takes over state management
- Universal compatibility – Works with MUI's
styledAPI, thesxprop, or external CSS modules through the globally available--mui-color-schemevariable - Accessibility compliance – Respects OS-level
prefers-color-schemesettings unless the user has explicitly chosen a different mode - SSR safety – Prevents hydration mismatches between server-rendered HTML and client-side theme detection
Step-by-Step Implementation Guide
Basic Setup with useColorScheme Hook
For applications using Material-UI v5.13 or later, the useColorScheme hook provides the most ergonomic integration. This hook, defined in packages/mui-material/src/useColorScheme/useColorScheme.js, automatically synchronizes with the values set by InitColorSchemeScript.
import * as React from 'react';
import { ThemeProvider, createTheme, CssBaseline, Switch } from '@mui/material';
import { useColorScheme } from '@mui/material/styles';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default function App() {
const { mode, setMode } = useColorScheme();
const theme = React.useMemo(
() => createTheme({ palette: { mode } }),
[mode]
);
const handleToggle = () => {
setMode(mode === 'light' ? 'dark' : 'light');
};
return (
<>
<InitColorSchemeScript />
<ThemeProvider theme={theme}>
<CssBaseline />
<Switch
checked={mode === 'dark'}
onChange={handleToggle}
inputProps={{ 'aria-label': 'toggle dark mode' }}
/>
</ThemeProvider>
</>
);
}
The setMode function automatically updates localStorage, dispatches the mui-color-scheme event, and modifies the data-mui-color-scheme attribute.
Manual Implementation Without Hooks
If you need custom logic or use an older version, read the attribute directly from the HTML element:
import * as React from 'react';
import { ThemeProvider, createTheme, CssBaseline, Switch } from '@mui/material';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default function App() {
const prefersDark = React.useMemo(() =>
document.documentElement.getAttribute('data-mui-color-scheme') === 'dark',
[]
);
const [mode, setMode] = React.useState<'light' | 'dark'>(
prefersDark ? 'dark' : 'light'
);
const theme = React.useMemo(
() => createTheme({ palette: { mode } }),
[mode]
);
const toggleColorScheme = () => {
const next = mode === 'light' ? 'dark' : 'light';
setMode(next);
localStorage.setItem('mui-color-scheme', next);
document.documentElement.setAttribute('data-mui-color-scheme', next);
window.dispatchEvent(new Event('mui-color-scheme'));
};
return (
<>
<InitColorSchemeScript />
<ThemeProvider theme={theme}>
<CssBaseline />
<Switch checked={mode === 'dark'} onChange={toggleColorScheme} />
</ThemeProvider>
</>
);
}
Server-Side Rendering with Next.js
For Next.js applications, place InitColorSchemeScript in your custom Document component to ensure it renders in the <head> before any stylesheets.
pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<InitColorSchemeScript nonce={process.env.NEXT_PUBLIC_NONCE} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
pages/_app.tsx
import { ThemeProvider, createTheme, CssBaseline } from '@mui/material';
import { useColorScheme } from '@mui/material/styles';
import { useEffect } from 'react';
export default function MyApp({ Component, pageProps }) {
const { mode } = useColorScheme();
const theme = React.useMemo(
() => createTheme({
palette: { mode },
cssVariables: true // Enable CSS variables for dynamic switching
}),
[mode]
);
// Sync body background to prevent flash during navigation
useEffect(() => {
document.body.style.backgroundColor = theme.palette.background.default;
}, [theme]);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
);
}
Technical Architecture
The implementation relies on coordination between three core files in the mui/material-ui repository:
packages/mui-material/src/InitColorSchemeScript/InitColorSchemeScript.js– Generates the inline script string that evaluateslocalStorageandmatchMedia('(prefers-color-scheme: dark)')to determine the effective schemepackages/mui-material/src/useColorScheme/useColorScheme.js– Provides React state management that subscribes to storage events and the custommui-color-schemeevent dispatched during manual togglespackages/mui-material/src/createTheme/createTheme.js– Consumes themodeparameter ('light'|'dark') to generate the appropriate palette as defined inpackages/mui-material/src/styles/cssVars/PaletteCssVars.js
The script executes the following logic synchronously:
// Simplified representation of the injected script
const saved = localStorage.getItem('mui-color-scheme');
const system = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const mode = saved || system;
document.documentElement.setAttribute('data-mui-color-scheme', mode);
document.documentElement.style.setProperty('--mui-color-scheme', mode);
This execution happens before the browser parses the <body>, ensuring CSS variables are available for the first paint.
Summary
- Position InitColorSchemeScript first in your HTML
<head>to block rendering until the color scheme is determined - The script checks
localStoragekeymui-color-schemeand falls back toprefers-color-schememedia query detection - Use
useColorSchemehook (v5.13+) to read and update the mode without manual DOM manipulation - For SSR, include the component in your Document class or root layout to prevent hydration mismatches
- The CSS variable
--mui-color-schemeand attributedata-mui-color-schemeprovide styling hooks for any CSS-in-JS solution
Frequently Asked Questions
Where should I place InitColorSchemeScript in my HTML?
Place it as the first child of your <head> element, before any <link> tags for stylesheets or other scripts. This positioning ensures the script executes synchronously before the browser begins painting, preventing any flash of incorrect theme colors.
Does InitColorSchemeScript work with Server-Side Rendering (SSR)?
Yes. When using Next.js or similar frameworks, include InitColorSchemeScript in your custom Document component (e.g., pages/_document.js). The component renders the initialization script into the server-sent HTML, which then executes immediately on the client before React hydration begins, as coordinated with packages/mui-material/src/useColorScheme/useColorScheme.js.
How does InitColorSchemeScript prevent the flash of wrong color scheme?
The component injects a blocking script that runs synchronously during HTML parsing. By setting the data-mui-color-scheme attribute and CSS variables immediately, the browser applies the correct colors during the initial paint rather than waiting for React to hydrate and calculate the theme, effectively eliminating FOUC.
Can I use InitColorSchemeScript without the useColorScheme hook?
Yes. You can manually read the color scheme from document.documentElement.getAttribute('data-mui-color-scheme') after the script runs. To update the theme, modify localStorage.setItem('mui-color-scheme', mode) and dispatch window.dispatchEvent(new Event('mui-color-scheme')) to notify other components, though the hook handles this synchronization automatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →