Material UI (MUI)がv5に更新されたので、このタイミングで改めてテーマ設定について記事を書きたいと思います。
Material UIは簡単にテーマを設定、カスタマイズすることが出来るのがメリットの一つです。
一方ドキュメントが少し見づらいため、具体的な実装が分かりづらいと感じる方もいらっしゃるかもしれません。
今回はMaterial UI v5で「1クリックで」テーマを切り替える方法と、テーマのカスタマイズ方法を解説していきます。
少し長い記事になりますが、段階を追って解説していきます。
こちらがこの記事で実装するサンプルです。
記事の最後におまけとして、「システム設定を読み取って初期のテーマを変更する」「パフォーマンス最適化」「テーマ設定をブラウザに保存」のセクションがあります。よければそちらも参考にして下さい。
結論だけ知りたいという方はGitHubでソースコードを公開していますのでそちらを見ていただければと思います。
こんな方におすすめ
- Material UI v5のテーマ切り替えの実装法を知りたい方
- 実際に動くサンプルを見たい方
- 公式ドキュメントは読んだがよく分からなかった方
ソースコード (結論だけ知りたい人向け)
こちらのGitHubから
GitHub : coders-shelf/mui-simple-theme-toggle
解説
セットアップ
まずはプロジェクトのセットアップから。create-react-appでプロジェクトを作成します。
npx create-react-app material-custom-theme
必要なパッケージをインストールします。v5になり必要なパッケージが変わっています。@muiに名前が変わっていることに注意して下さい。
yarn add @mui/material @emotion/react @emotion/styled
アイコンも後々使うことになるのでインストールしておきます。
yarn add @mui/icons-material
ついでに気になる方は不要なファイルを削除しておいて下さい。
(App.js, index.js, index.css以外は削除してかまいません。)
テーマ設定
出来るだけ最小限のコードでダークモードを設定していきます。
大まかな流れ ↓↓↓
- createThemeでテーマを作成
- ThemeProviderでコンポーネントをラップする
- ThemeProviderに作成したテーマを渡す
コードは以下の通りです。createThemeに渡すオブジェクトでテーマのカスタマイズができます。
mode: 'dark'ならダークモードになり、mode: 'light'を指定すればライトモードになります。
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
const App = () => {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Button variant='contained'>Hello World</Button>
</ThemeProvider>
);
}
export default App;
これでダークモードがデフォルトで表示されるようになります。
modeをlightにすればlightモードが設定できます。
const lightTheme = createTheme({
palette: {
mode: 'light',
},
});
次にテーマの切り替えをボタンで出来るように準備をしていきます。テーマ切り替えボタンはヘッダーに配置することが多いかと思いますので、MUIのAppBarコンポーネントを使っていきます。
大まかな流れ ↓↓↓
- AppBarを作成
- テーマ切り替え用のアイコンボタンを設置
- 表示させる
一番簡単なAppBarのサンプルからほぼコピペします。
MUI公式ドキュメント: basic-app-bar
そこから不要なアイコンを消し、ログインボタンをアイコンボタンに変更します。アイコンはInvertColorsアイコンを使用します。
アイコンは以下のリンクから検索できます。
MUI公式ドキュメント: material-icons
ソースコードはこのようになります。
src > common > CustomAppBar.jsを作成
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import InvertColorsIcon from '@mui/icons-material/InvertColors';
const CustomAppBar = () => {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position='static'>
<Toolbar>
<Typography variant='h6' component='div' sx={{ flexGrow: 1 }}>
MUI v5でテーマをカスタマイズする
</Typography>
<IconButton color='inherit'>
<InvertColorsIcon />
</IconButton>
</Toolbar>
</AppBar>
</Box>
);
};
export default CustomAppBar;
作成したAppBarを表示させます。
src > App.jsに<CustomAppBar />を入れます。
const App = () => {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<CustomAppBar />
<Button variant='contained'>Hello World</Button>
</ThemeProvider>
);
};
以上で準備が整いました。次のセクションから具体的な実装をしていきます。
テーマの切り替え
テーマを切り替えるには、簡単に言えばThemeProviderに渡すテーマをボタンを押すごとに切り替える、ただそれだけです。
useStateを使ってmodeというstateに'light'か'dark'の文字列を持たせます。
このmodeというstateを元にthemeを切り替えます。
src > App.js
import { useState } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import CustomAppBar from './components/common/CustomAppBar';
const App = () => {
const [mode, setMode] = useState('dark');
const theme = createTheme({
palette: {
mode,
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<CustomAppBar />
<Button variant='contained'>Hello World</Button>
</ThemeProvider>
);
};
export default App;
次にこのmodeを他のコンポーネントから操作出来るように実装します。modeを変更するハンドラを作成し、propsとして子コンポーネントに渡していってもいいのですが、より汎用的なコードにするため、Contextを使います。
メモ
このレベルの小さいコンポーネントではともかく実際にウェブサイトを作成する際は、階層の離れたコンポーネントから操作することになるのでContextを使います。
今回で言うとAppBarコンポーネントからテーマを切り替えられるようにしていきます。
大まかな流れ ↓↓↓
- Contextを作成
- Context.Providerでコンポーネントをラップする
- useContextでコンテキストを使う (Consumer)
まずはコンテキストを作成します。colorModeというオブジェクトをcontext valueに設定すると想定し、toggleColorModeというテーマを切り替えるハンドラをcolorModeオブジェクト内に設定します。
src > context > ColorModeContext.js
import { createContext } from 'react';
const ColorModeContext = createContext({ toggleColorMode: () => {} });
export default ColorModeContext;
src > App.jsで全体をコンテキストプロバイダでおおいます。
<ColorModeContext.Provider>
<ThemeProvider theme={theme}>
...略
</ThemeProvider>
</ColorModeContext.Provider>
プロバイダにcolorModeオブジェクトをcontext valueとして設定します。先にも述べた通り、colorModeオブジェクトにはtoggleColorModeというテーマを切り替えるハンドラが設定されています。
src > App.js
import { useState } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';
import CustomAppBar from './components/common/CustomAppBar';
import ColorModeContext from './context/ColorModeContext';
const App = () => {
const [mode, setMode] = useState('dark');
const theme = createTheme({
palette: {
mode,
},
});
const colorMode = {
toggleColorMode: () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
},
};
return (
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<CssBaseline />
<CustomAppBar />
<Button variant='contained'>Hello World</Button>
</ThemeProvider>
</ColorModeContext.Provider>
);
};
export default App;
最後にAppBarのテーマ切り替えボタンが押されたときの処理を実装します。(Consumer側の実装)
useContextを使ってcolorModeオブジェクトを取り出し、ボタンにtoggleColorModeハンドラを設定します。
src > common > CustomAppBar.js
import { useContext } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import InvertColorsIcon from '@mui/icons-material/InvertColors';
import ColorModeContext from '../../context/ColorModeContext';
const CustomAppBar = () => {
const colorMode = useContext(ColorModeContext);
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position='static'>
<Toolbar>
<Typography variant='h6' component='div' sx={{ flexGrow: 1 }}>
MUI v5でテーマをカスタマイズする
</Typography>
<IconButton color='inherit' onClick={colorMode.toggleColorMode}>
<InvertColorsIcon />
</IconButton>
</Toolbar>
</AppBar>
</Box>
);
};
export default CustomAppBar;
これでダークモードとライトモードとの切り替えは完成です。
以下のようにボタンでテーマを切り替えられるようになっているはずです。
このままでもいいですが、せっかくなのでテーマをカスタマイズして使ってみましょう。
テーマのカスタマイズ
ライトモード、ダークモードの各テーマについて色のカスタマイズをしてみましょう。
大まかな流れ ↓↓↓
- 使いたい色を用意する
- createThemeのパレットに設定する
- 条件分岐を使ってライトモード、ダークモードそれぞれに設定
まずは公式ドキュメントを参考に使いたい色をインポートします。
MUI公式ドキュメント: color
今回は以下の3つの色をインポートして使ってみます。
import { blueGrey, brown, grey } from '@mui/material/colors';
次にsrc > App.js 内のcreateThemeの箇所を編集します。modeの値、つまり'light', 'dark'に応じて条件分岐させ、paletteオブジェクトを変更します。
createTheme({
palette: {
mode,
...(mode === 'light'
? {
primary: blueGrey,
divider: blueGrey[200],
text: {
primary: grey[900],
secondary: grey[800],
},
}
: {
primary: brown,
divider: brown[700],
background: {
default: brown[900],
paper: brown[900],
},
text: {
primary: '#fff',
secondary: brown[500],
},
}),
},
})
「...」はjavascriptのスプレッド構文で、lightモードであればpaletteが次のようになります。
palette: {
'light',
primary: blueGrey,
divider: blueGrey[200],
text: {
primary: grey[900],
secondary: grey[800],
},
}
スプレッド構文について知りたい方は以下のリンクから。
以上で完成です。以下のgifのようにテーマの切り替えが出来るはずです。
(文字色の確認のためテキストを追加してあります。)
以下はおまけです。
おまけ:システム設定を読み取る
手動でテーマを切り替えるだけでなく、ブラウザの設定にしたがってテーマを設定したい場合もあります。
CSSのprefers-color-schemeというメディアクエリでユーザがダークモードを設定しているかを確認できます。
Material UIにはuseMediaQueryというhookが用意されており、それを利用して値を取り出すことが出来ます。
useEffectを利用することでこの値が変更されたときにテーマを変更できます。また最初に実行されるときに初期値を上書きしてくれます。
src > App.js
import useMediaQuery from '@mui/material/useMediaQuery';
// 途中は省略
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
useEffect(() => {
if (prefersDarkMode) {
setMode('dark');
} else {
setMode('light');
}
}, [prefersDarkMode]);
prefersDarkModeにはtrue, falseのBoolean値が入ります。
これで以下のようにシステム設定に応じたテーマ変更が可能です。(もちろんボタンを押して手動で切り替えることもできます)
補足:
useMediaQuery('(prefers-color-scheme: dark)')の箇所で初期値が必ずfalseになってしまうため、レンダリング回数が一回多くなってしまうことがあります。
useMediaQueryに{noSsr: true}というオプションを付けるとこの問題を回避できます。
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {
noSsr: true,
});
おまけ:メモ化 (パフォーマンス最適化)
公式ドキュメントでuseMemoを使った最適化が行われているため、その解説もしていきます。
この記事で書いているような小さなコンポーネントでは問題になりませんが、より本格的に開発していく場合はパフォーマンス最適化が必要になるかもしれません。
メモ
パフォーマンス最適化はしなくてもいいならしない、が基本です。最適化のコードそれ自体の実行コストとソースコードの複雑化のためです。
まずはテーマオブジェクトを作成するcreateThemeですが、現状コンポーネントが再レンダリングされるごとにcreateThemeが実行されています。
テーマの場合はmodeが変更された場合のみcreateThemeすればいいのでこれをメモ化します。
src > App.js
const theme = useMemo(
() =>
createTheme({
palette: {
mode,
},
}),
[mode]
);
続いてcolorModeオブジェクトです。
const colorMode = useMemo(
() => ({
toggleColorMode: () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
},
}),
[]
);
colorModeオブジェクトは再実行の必要はありませんので、一度だけ実行されるように依存配列は空になっています。
(ダーク/ライト)テーマの設定をブラウザに保存しておきたい
ユーザが手動でテーマを切り替えた時、その設定をブラウザに保存しておくことも出来ます。
またユーザが次にページを訪れたときに自動でそのテーマに切り替えるようになります。
今回はlocalStorageにその設定を保存しておきます。
src > App.js
toggleColorMode: () => {
setMode((prevMode) => {
if (prevMode === 'light') {
localStorage.setItem('paletteMode', 'dark');
return 'dark';
} else {
localStorage.setItem('paletteMode', 'light');
return 'light';
}
});
},
useEffectのロジックを修正します。ユーザが手動で選んだテーマを優先し、それがなければシステム設定を元にテーマを設定します。
useEffect(() => {
if (localStorage.getItem('paletteMode') === 'dark') {
setMode('dark');
} else if (localStorage.getItem('paletteMode') === 'light') {
setMode('light');
} else if (prefersDarkMode) {
setMode('dark');
} else {
setMode('light');
}
}, [prefersDarkMode]);
*実際は有効期限を設定できるCookieを使った方がいいかもしれません。
終わり