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-scheme media query
  • Reads persisted user selections from localStorage under the key mui-color-scheme
  • Sets global indicators by writing to the CSS custom property --mui-color-scheme and the HTML element's data-mui-color-scheme attribute
  • 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 styled API, the sx prop, or external CSS modules through the globally available --mui-color-scheme variable
  • Accessibility compliance – Respects OS-level prefers-color-scheme settings 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:

  1. packages/mui-material/src/InitColorSchemeScript/InitColorSchemeScript.js – Generates the inline script string that evaluates localStorage and matchMedia('(prefers-color-scheme: dark)') to determine the effective scheme
  2. packages/mui-material/src/useColorScheme/useColorScheme.js – Provides React state management that subscribes to storage events and the custom mui-color-scheme event dispatched during manual toggles
  3. packages/mui-material/src/createTheme/createTheme.js – Consumes the mode parameter ('light' | 'dark') to generate the appropriate palette as defined in packages/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 localStorage key mui-color-scheme and falls back to prefers-color-scheme media query detection
  • Use useColorScheme hook (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-scheme and attribute data-mui-color-scheme provide 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →