Gatsby + MUIでダークモードを実装する
ブログをダークモードに対応させてみた。
ユーザーがダークモード/ライトモードを変更できるようにすると以外と面倒だったので、その作業記録。
gatsby-plugin-dark-modeをインストールして試してみる
まずは最も使われているであろうgatsby-plugin-dark-modeを試すことにした。
こちらを参考に実施。通常のgatsby pluginのインストールといっしょ。
まずはnpm/yarnで当該プラグインをインストール。
yarn add gatsby-plugin-dark-mode
gatsby-config.jsに追加。
module.exports = {
plugins: ['gatsby-plugin-dark-mode'],
}
プラグインがprefers-color-scheme
メディアクエリの値を参照してくれるので、OS/ブラウザがダークモード設定ならば勝手にダークモードがデフォルトになる。このプラグインがやることは
- localstorageに値が入る
- CSSのmainにモードによるclassを付与する
という設定をしてくれるだけ。
参考にしたサイトではトグルするためのコンポーネントを作っているが、
これは後述する。
作らなくてもOSデフォルトの設定は読み込んでくれるはず。
また、ダークモード用のCSSは自分で書く必要がある。
body {
--bg: white;
--link: #444;
background-color: var(--bg);
margin: 0
}
body.dark {
--bg: black;
--link: white;
background-color: var(--bg);
}
a {
color: var(--link);
}
CSS Variableで完結できるならこれでおしまい。今回はここからが難しかった...
MUIのテーマを併せて動的に変更できるようにしたい
本ブログではMUI(旧Material-UI)を利用している。
MUIにはtheme指定時にモード指定があり、これを切り替えるとMUIの各コンポーネントが
自動的にダークモードになる。
MUIの公式ページでは通常のThemeProviderでは以下のようにダークモード設定している。
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});
function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<main>This app is using the dark mode</main>
</ThemeProvider>
);
}
export default App;
ただ、ダークモードとライトモードをトグルするとなると、変更があったことを検知して動作を変える必要がある。
これにあたっては以下の問題がある。
- media query(prefers-color-scheme)やlocalStorageはレンダリング地点では読み取れない
- useEffect等で遅延して上記を読み取って動作を変えようとすると、一瞬ライトモードが表示されてからダークモードになって画面がチカチカしてしまう(いわゆるFOUC)
CssVarsProviderでFOUCを回避する
これの回避が難しそうで四苦八苦していたが、MUI側でCSS Variablesを用いることでFOUCを回避する実験的APIが公開されており、これを用いることで解決できた。
以下のコードは実際のコードから関連する箇所のみを切り取ってきたものなので適宜補完が必要かもしれない。
import React from 'react';
import { getInitColorSchemeScript } from '@mui/material/styles';
export const onRenderBody = ({ setHtmlAttributes, setPreBodyComponents, setBodyAttributes}) => {
setPreBodyComponents([
<React.Fragment key="mui-init-color-scheme-script">
{getInitColorSchemeScript({defaultMode: "system"})}
</React.Fragment>,
]);
setHtmlAttributes({ lang: "ja" })
}
bodyの読み込みの前に現在の色設定を読み出すため、gatsby-ssr.jsの中でgetInitColorSchemeScript()
を呼び出す必要がある。
ここで、defaultModeを"system"にしてあげないと、prefers-color-schemeの値を読み取らず、OSのダークモード設定に準拠できないようだ。
import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material/styles';
//適当なコンポーネント内
const Layout = ({ title, tags, categories, children }) => {
return (
<CssVarsProvider defaultMode="system">
<CssBaseline />
<LayoutContent title={title} tags={tags} categories={categories} children={children}/>
</CssVarsProvider>
)
MUIのThemeProviderを使っていた場所をCssVarsProviderに変更する。
const LayoutContent = ({title, tags, categories, children}) => {
const { mode, setMode } = useColorScheme();
const [mounted, setMounted] = React.useState(false);
const darkMode = useDarkMode(); //MUI以外のダークモード切替
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
//必要箇所のみ抜粋。以下のようなコンポーネントを書く
return (
<Tooltip title="ダークモードのON/OFF設定を変更できます">
<IconButton sx={{ ml: 1, marginRight: "1em" }} onClick={e => {
if(darkMode.value){
darkMode.disable()
setMode('light');
} else {
darkMode.enable()
setMode('dark');
}
}} color="inherit">
{ darkMode.value ? <Brightness7Icon /> : <Brightness4Icon color="primary" />}
</IconButton>
}
適当なコンポーネントの中でuseColorScheme()
を呼び出す。(ただしCssVarsProviderの中にあるコンポーネントでなければならない)
MUI以外のCSSの置き換えにはgatsby-plugin-use-dark-modeも併用した。(紆余曲折の結果前述のgatsby-plugin-dark-modeでなくこちらを利用した。がんばれば使わなくてもできそうだが今回はこの形に)
これでチカチカせずに済むようになった!