MUIのテーマ入門 — createThemeで色・フォント・余白を一元管理する
この章の目次開く
- 設定しなくてもMUIは動く
- ThemeProviderが必要になる場面
- テーマが持っているもの
- palette — 色
- mainだけ書けばいい
- typography — フォント
- spacing — 余白の基準
- shape — 角丸
- テーマを配る — ThemeProviderと入れ子
- 外側のテーマを引き継ぐ
- コンポーネントからテーマを読む
- 1. 文字列で参照する(いちばん手軽)
- 2. sx を関数にする
- 3. useTheme フック
- よくあるハマりどころ
- テーマを変えたのに反映されない
- 入れ子にしたら外側の設定まで消えた
- TypeScriptで独自の色を足すと型エラーになる
- ダークモードの切り替えが効かない
- ちゃんと使うためのポイント
- 参考リンク
MUIで色やフォントを変えるとき、コンポーネントを1つずつ sx で塗り替えていくと、すぐに破綻します。同じ青を何十か所に書くことになり、ブランドカラーを変えたいときに全置換が必要になります。
そのための仕組みがテーマです。テーマは、アプリ全体で共有するデザインの設定値をまとめたオブジェクトで、createTheme で作ります。
学習者sx で毎回 color: '#1976d2' って書いてたんだけど、それってダメなやり方だったの…?
動きはしますが、変更に弱くなります。テーマに1か所書いておけば、あとは color: 'primary.main' で参照するだけになります。
設定しなくてもMUIは動く
本題に入る前に、前提を1つ押さえておきます。
テーマを設定しなくても、MUIのコンポーネントはそのまま動きます。<Button> を置けばMUI標準の青いボタンが出ますし、sx={{ color: 'primary.main' }} もちゃんと色が付きます。
仕組みはこうです。MUIは useTheme() でテーマを取り出しますが、その実装は「テーマが配られていなければ既定のテーマを使う」という形になっています。
// @mui/material/styles/useTheme.js
const theme = useTheme(defaultTheme);この defaultTheme は、createTheme() を引数なしで呼んだ結果です。つまり設定を書かなかった場合でも、完全なテーマが1つ必ず存在しているということになります。
ThemeProviderが必要になる場面
| やりたいこと | ThemeProvider |
|---|---|
| MUIの見た目のまま使う | 要らない |
| 色・フォント・角丸を変える | 要る |
| ダークモードに対応する | 要る |
theme.components で既定のpropsを変える | 要る |
判断の軸は1つだけで、既定から変えたいことがあるかどうかです。変えないなら置く意味はありません。
先生「設定が効かない」系のトラブルは、設定の中身より先に配線を疑うと早く解決します。テーマは作るだけでは何もしません。
テーマが持っているもの
createTheme() が返すオブジェクトには、次のような区画があります。
| キー | 中身 |
|---|---|
palette | 色。primary / secondary / error など |
typography | フォントファミリと、見出し・本文のサイズ |
spacing | 余白の基準値(デフォルト8px) |
shape | 角丸の基準値(デフォルト4px) |
breakpoints | レスポンシブの区切り幅 |
shadows | 影のプリセット(25段階) |
zIndex | 重なり順。モーダルやツールチップの前後関係 |
transitions | アニメーションの時間とイージング |
components | コンポーネントごとの既定値と見た目の上書き |
上書きしたいものだけを createTheme に渡します。書かなかった項目はMUIの既定値がそのまま使われます。
import { createTheme } from '@mui/material/styles';
export const theme = createTheme({
palette: {
primary: { main: '#0b5cad' },
},
shape: {
borderRadius: 8,
},
});palette — 色
palette には、役割ごとに色の枠が用意されています。
| キー | 意味 |
|---|---|
primary | 主役の色。ボタンやリンクに使われる |
secondary | 補助の色 |
error | エラー表示 |
warning | 警告 |
info | 情報 |
success | 成功 |
grey | グレースケール。grey.100 〜 grey.900 |
text / background / divider / action | 文字色・背景・区切り線・操作状態 |
それぞれの色は4つの値を持ちます。
palette: {
primary: {
main: '#0b5cad', // 基本の色
light: '#4a89df', // 明るいバリエーション
dark: '#00337e', // 暗いバリエーション
contrastText: '#fff', // この色の上に乗せる文字色
},
}mainだけ書けばいい
実際には main だけ指定すれば足ります。light と dark は main から自動生成され、contrastText も自動で決まります。
palette: {
primary: { main: '#0b5cad' },
}
typography — フォント
フォントファミリと、文字スタイルのプリセットを持ちます。
typography: {
fontFamily: '"Noto Sans JP", sans-serif',
h1: { fontSize: '2.5rem', fontWeight: 700 },
}用意されているプリセットは次の13種類です。
| 分類 | バリアント |
|---|---|
| 見出し | h1 h2 h3 h4 h5 h6 |
| 小見出し | subtitle1 subtitle2 |
| 本文 | body1 body2 |
| その他 | button caption overline |
Typography コンポーネントの variant で呼び出します。
ここではテーマ側の設定だけを扱います。Typography コンポーネント自体の使い方(component との違い、noWrap、余白の扱い)はTypographyの章にまとめてあります。
<Typography variant="h4">セクション見出し</Typography>
<Typography variant="body2" color="text.secondary">補足のテキスト</Typography>spacing — 余白の基準
前の章で触れた8pxの基準値は、ここで変更できます。
const theme = createTheme({ spacing: 4 });
// この設定では mt: 2 が 8px になる変更すると sx の数値の意味が全部変わるので、プロジェクトの最初に決めて動かさないのが無難です。
shape — 角丸
shape.borderRadius のデフォルトは 4px です。これを変えると、ボタン・カード・入力欄など角丸を持つコンポーネントが一斉に変わります。
shape: { borderRadius: 12 },「MUIっぽさ」を薄めたいとき、最も効果が大きいのは角丸とフォントの変更です。色だけ変えてもMUIらしさは残りますが、この2つを触ると印象が大きく動きます。
テーマを配る — ThemeProviderと入れ子
作ったテーマは ThemeProvider で配ります。基本はアプリ全体を1つで包む形で、置き方はNext.js App Routerへの導入で扱いました。
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>ThemeProvider はここで終わりではありません。入れ子にすると、その内側だけ別のテーマにできます。
<ThemeProvider theme={theme}>
<Header />
<ThemeProvider theme={adminTheme}>
<AdminPanel /> {/* ここだけ別テーマ */}
</ThemeProvider>
</ThemeProvider>管理画面だけ配色を変える、特定のセクションだけ角丸を強くする、といった場面で使います。
外側のテーマを引き継ぐ
上の書き方には落とし穴があります。adminTheme を createTheme({ palette: { primary: ... } }) で作った場合、外側のテーマの設定は引き継がれません。内側は独立した別のテーマになります。
外側を土台にして一部だけ変えたいときは、theme に関数を渡します。
<ThemeProvider theme={(outerTheme) => ({
...outerTheme,
palette: {
...outerTheme.palette,
primary: { main: '#c2185b' },
},
})}>
<AdminPanel />
</ThemeProvider>型定義上も theme は Partial<Theme> | ((outerTheme: Theme) => Theme) となっており、コメントに「外側のテーマを拡張するために関数を渡せる」と書かれています。
学習者createTheme で作り直すのと、関数で受け取るのって何が違うの?
先生作り直すと、外側で設定したフォントや余白の基準まで既定値に戻ります。「色だけ変えたい」つもりが全部変わってしまう、という事故が起きます。一部だけ変えるなら関数形式です。
コンポーネントからテーマを読む
テーマの値は、3つの方法で取り出せます。
1. 文字列で参照する(いちばん手軽)
<Box sx={{ color: 'primary.main', bgcolor: 'grey.100' }} />2. sx を関数にする
計算が必要なときや、shadows のように配列を引きたいときに使います。
<Box sx={(theme) => ({ boxShadow: theme.shadows[3] })} />3. useTheme フック
JavaScriptのロジックでテーマの値を使いたいときに使います。
'use client';
import { useTheme } from '@mui/material/styles';
function Chart() {
const theme = useTheme();
return <Line color={theme.palette.primary.main} />;
}
先生スタイルを当てるだけなら1番で十分です。useTheme はフックなので 'use client' が必要になります。グラフライブラリに色を渡すときなど、JS側で値が要る場面に限って使ってください。
よくあるハマりどころ
テーマを変えたのに反映されない
まず ThemeProvider を置いているか確認します。配り忘れていても既定のテーマで動いてしまうため、エラーが出ません。置いてある場合は、ThemeProvider の外側にコンポーネントがあるか、createTheme を呼び直していないケースです。テーマオブジェクトはモジュールのトップレベルで1回作り、使い回してください。コンポーネントの中で毎回 createTheme() を呼ぶと、再レンダーのたびに別のテーマが生まれて描画が重くなります。
入れ子にしたら外側の設定まで消えた
内側の ThemeProvider に createTheme() で作った別テーマを渡しています。外側を土台にしたいときは theme={(outerTheme) => ...} の関数形式を使います。
TypeScriptで独自の色を足すと型エラーになる
palette に brand のような独自キーを足すには、型の拡張宣言が必要です。declare module '@mui/material/styles' でインターフェースを拡張します。
ダークモードの切り替えが効かない
v9では、単に palette.mode を切り替えるだけでなく、CSS変数と colorSchemes を使う方式が用意されています。これは専用の章で扱います。
ちゃんと使うためのポイント
-
テーマを設定しなくてもMUIは既定テーマで動く。
ThemeProviderが要るのは既定から変えたいときだけ createThemeしてもThemeProviderで配らなければ、エラーも出ず既定のまま- テーマは上書きしたい項目だけ渡す。書かなかった値は既定値が残る
paletteの色はmainだけ書けば、明暗と文字色は自動生成される- コントラスト比がWCAGの3:1を下回ると、コンソールに警告が出る
spacingはプロジェクト開始時に決めて動かさない- 「MUIっぽさ」を薄めるなら
shape.borderRadiusとtypography.fontFamily - テーマオブジェクトはモジュールのトップレベルで1回だけ作る
-
ThemeProviderは入れ子にできる。一部だけ変えるならthemeに関数を渡して外側を引き継ぐ
次の章では、テーマを踏まえてカスタマイズの4段階を扱います。sx と styled() とテーマの上書きを、どう使い分けるかという話です。