本文へスキップ
ウェブエンジニア問題集
第5章

MUIのテーマ入門 — createThemeで色・フォント・余白を一元管理する

約13分
この章の目次開く

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);
js

この 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,
  },
});
tsx

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', // この色の上に乗せる文字色
  },
}
tsx

mainだけ書けばいい

実際には main だけ指定すれば足ります。light と dark は main から自動生成され、contrastText も自動で決まります。

palette: {
  primary: { main: '#0b5cad' },
}
tsx
色を設計するイメージ

typography — フォント

フォントファミリと、文字スタイルのプリセットを持ちます。

typography: {
  fontFamily: '"Noto Sans JP", sans-serif',
  h1: { fontSize: '2.5rem', fontWeight: 700 },
}
tsx

用意されているプリセットは次の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>
tsx

spacing — 余白の基準

前の章で触れた8pxの基準値は、ここで変更できます。

const theme = createTheme({ spacing: 4 });
// この設定では mt: 2 が 8px になる
tsx

変更すると sx の数値の意味が全部変わるので、プロジェクトの最初に決めて動かさないのが無難です。


shape — 角丸

shape.borderRadius のデフォルトは 4px です。これを変えると、ボタン・カード・入力欄など角丸を持つコンポーネントが一斉に変わります。

shape: { borderRadius: 12 },
tsx

「MUIっぽさ」を薄めたいとき、最も効果が大きいのは角丸とフォントの変更です。色だけ変えてもMUIらしさは残りますが、この2つを触ると印象が大きく動きます。


テーマを配る — ThemeProviderと入れ子

作ったテーマは ThemeProvider で配ります。基本はアプリ全体を1つで包む形で、置き方はNext.js App Routerへの導入で扱いました。

<ThemeProvider theme={theme}>
  <CssBaseline />
  {children}
</ThemeProvider>
tsx

ThemeProvider はここで終わりではありません。入れ子にすると、その内側だけ別のテーマにできます。

<ThemeProvider theme={theme}>
  <Header />
  <ThemeProvider theme={adminTheme}>
    <AdminPanel /> {/* ここだけ別テーマ */}
  </ThemeProvider>
</ThemeProvider>
tsx

管理画面だけ配色を変える、特定のセクションだけ角丸を強くする、といった場面で使います。

外側のテーマを引き継ぐ

上の書き方には落とし穴があります。adminTheme を createTheme({ palette: { primary: ... } }) で作った場合、外側のテーマの設定は引き継がれません。内側は独立した別のテーマになります。

外側を土台にして一部だけ変えたいときは、theme に関数を渡します。

<ThemeProvider theme={(outerTheme) => ({
  ...outerTheme,
  palette: {
    ...outerTheme.palette,
    primary: { main: '#c2185b' },
  },
})}>
  <AdminPanel />
</ThemeProvider>
tsx

型定義上も theme は Partial<Theme> | ((outerTheme: Theme) => Theme) となっており、コメントに「外側のテーマを拡張するために関数を渡せる」と書かれています。

学習者学習者

createTheme で作り直すのと、関数で受け取るのって何が違うの?

先生先生

作り直すと、外側で設定したフォントや余白の基準まで既定値に戻ります。「色だけ変えたい」つもりが全部変わってしまう、という事故が起きます。一部だけ変えるなら関数形式です。


コンポーネントからテーマを読む

テーマの値は、3つの方法で取り出せます。

1. 文字列で参照する(いちばん手軽)

<Box sx={{ color: 'primary.main', bgcolor: 'grey.100' }} />
tsx

2. sx を関数にする

計算が必要なときや、shadows のように配列を引きたいときに使います。

<Box sx={(theme) => ({ boxShadow: theme.shadows[3] })} />
tsx

3. useTheme フック

JavaScriptのロジックでテーマの値を使いたいときに使います。

'use client';
import { useTheme } from '@mui/material/styles';
 
function Chart() {
  const theme = useTheme();
  return <Line color={theme.palette.primary.main} />;
}
tsx
先生先生

スタイルを当てるだけなら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() とテーマの上書きを、どう使い分けるかという話です。


参考リンク