MUIのダークモード対応 — cssVariablesとcolorSchemesで切り替える
この章の目次開く
MUIのダークモード対応は、v5の時代から仕組みが入れ替わっています。いま主流なのは、色をCSS変数として出力し、HTMLの属性を書き換えて切り替える方式です。
古い記事にある palette.mode を状態で持ち回す方法も動きますが、書き方も挙動も違います。この章では現行の方式を扱います。
学習者ダークモードって、useState でlightとdarkを切り替えて createTheme を作り直すんじゃないの?
それが旧来のやり方です。テーマを作り直すので、切り替えるたびにアプリ全体が再レンダーされます。CSS変数方式なら、属性を1つ書き換えるだけで色が変わり、Reactの再レンダーが起きません。
最小構成
まずテーマ側で2つの設定を有効にします。
// src/theme.ts
'use client';
import { createTheme } from '@mui/material/styles';
export const theme = createTheme({
cssVariables: true,
colorSchemes: { light: true, dark: true },
});| 設定 | 意味 |
|---|---|
cssVariables: true | テーマの値をCSS変数として出力する |
colorSchemes | 用意する配色。true を渡すとMUIの既定色が使われる |
あとは通常どおり ThemeProvider と CssBaseline で包みます。
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>これだけで、OSの設定がダークなら自動的にダーク表示になります。 切り替えUIを作らないなら、ここで完成です。
切り替えUIを作る — useColorScheme
ユーザーが自分で選べるようにするには useColorScheme フックを使います。
'use client';
import { useColorScheme } from '@mui/material/styles';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
export function ModeSelect() {
const { mode, setMode } = useColorScheme();
return (
<Select value={mode} onChange={(e) => setMode(e.target.value as 'light' | 'dark' | 'system')}>
<MenuItem value="system">OSに合わせる</MenuItem>
<MenuItem value="light">ライト</MenuItem>
<MenuItem value="dark">ダーク</MenuItem>
</Select>
);
}このフックが返す主な値は次のとおりです。
| 値 | 中身 |
|---|---|
mode | ユーザーが選んだ設定。'light' / 'dark' / 'system' |
systemMode | mode が 'system' のときの実際の値 |
colorScheme | いま適用されている配色 |
setMode() | 設定を変更する。localStorageにも保存される |
サーバーでは値がundefinedになる
ここが最大の落とし穴です。型定義にも明記されていますが、mode と colorScheme はサーバー側では必ず undefined になります。ユーザーの選択はブラウザのlocalStorageにあり、サーバーには知りようがないためです。
そのまま書くと、サーバーが描画したHTMLとブラウザが描画した結果が食い違い、ハイドレーションの警告が出ます。
対策は、マウントが完了するまで中身を描かないことです。
'use client';
export function ModeSelect() {
const { mode, setMode } = useColorScheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
// 高さだけ確保しておくとレイアウトがずれない
return <Box sx={{ width: 140, height: 40 }} />;
}
return <Select value={mode} /* ... */ />;
}
先生「ダークモードのトグルだけハイドレーションエラーが出る」という相談はとても多いです。原因はほぼこれで、MUI固有というよりサーバー描画全般の性質です。
初回表示のちらつきを防ぐ
切り替えUIを付けると、新しい問題が出ます。
ユーザーがダークを選んで再訪したとき、サーバーが返すHTMLはライトのままです。ブラウザでJavaScriptが動いてlocalStorageを読んだ瞬間にダークへ切り替わるため、一瞬だけ白い画面が光ります。
学習者これ、他のサイトでも見たことある…。目に痛いやつ。
これを防ぐのが InitColorSchemeScript です。Reactが動くより前に、localStorageを読んでHTML要素へ属性を付けるスクリプトを埋め込みます。
// app/layout.tsx
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" suppressHydrationWarning>
<body>
<InitColorSchemeScript attribute="data-mui-color-scheme" />
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}<body> の一番最初に置くのが要点です。あとに置くと、その前の要素が描画されてからスクリプトが動くため、ちらつきが残ります。
既定値
設定を省略したときの値は、型定義から確認できます。
| 項目 | 既定値 |
|---|---|
| HTML属性 | data-mui-color-scheme |
| モードの保存キー | mui-mode |
| 配色の保存キー | mui-color-scheme |
| 初回訪問時のモード | system |
localStorageに mui-mode というキーが増えるのはこのためです。開発者ツールで確認すると、切り替えの動きが追いやすくなります。

colorSchemeSelector — 切り替えの合図を変える
MUIは既定で data-mui-color-scheme 属性を見て配色を決めます。この「合図」は変更できます。
createTheme({
cssVariables: { colorSchemeSelector: 'class' },
colorSchemes: { light: true, dark: true },
});指定できるのは 'media' / 'class' / 'data' と、任意のセレクタ文字列です。
| 指定 | 動き |
|---|---|
'media' | OSの設定だけに従う。ユーザーによる切り替えはしない |
'class' | <html class="dark"> のようなクラスで切り替える |
'data' | data属性で切り替える |
| 任意の文字列 | 独自のセレクタを指定する |
CSS変数として何が出ているか
cssVariables: true にすると、テーマの値がCSSカスタムプロパティとして出力されます。ブラウザの開発者ツールで <html> 要素を見ると、--mui-palette-primary-main のような変数が並んでいるのが確認できます。
配色の切り替えは、この変数の値を差し替えることで行われます。JavaScriptが色を計算し直してコンポーネントを再描画するわけではないので、切り替えが速く、ちらつきも少なくなります。
よくあるハマりどころ
ダークにしても背景が白いまま
CssBaseline を置いていない可能性があります。body の背景色はこのコンポーネントが当てています。
自分で書いた色だけダークで見えなくなる
sx={{ color: '#333' }} のように生の値を書いた箇所は、配色が変わっても追従しません。color: 'text.primary' のようにテーマのパスで参照してください。テーマ経由の色は、配色ごとに値が切り替わります。
ハイドレーションの警告が消えない
<html> に suppressHydrationWarning を付けているか確認してください。InitColorSchemeScript はサーバーが返したHTMLの属性を書き換えるため、Reactから見ると差分として検出されます。これは意図した動作なので、警告を抑止するのが正しい対処です。
切り替えたのにリロードで元に戻る
InitColorSchemeScript が無いか、置き場所が <body> の先頭でない可能性があります。保存自体はlocalStorageに効いているので、開発者ツールで mui-mode の値を確認すると切り分けられます。
ちゃんと使うためのポイント
-
v9のダークモードは
cssVariables: trueとcolorSchemesの2点セット - 切り替えUIが不要なら、この2行だけでOSの設定に追従する
useColorSchemeのmodeはサーバーでundefined。マウント後に描くこと- ちらつき対策の
InitColorSchemeScriptは<body>の先頭に置く <html>にはsuppressHydrationWarningが必要- 色は生の値ではなくテーマのパスで参照する
次の章では、ブレークポイントとuseMediaQueryを使ったレスポンシブ対応を扱います。