ページ全体の構成
ページ全体の構成です。
import React from 'react';
import ToggleSwitchButton from './components/ToggleSwitchButton'
const App = () => {
return (
<div>
<header className="header">
<div>LOGO</div>
<ToggleSwitchButton
className="toggle-switch-button"
offColor="#ccc"
onColor="#383896"
size="1em"
/>
</header>
<div className="container">
<div>example</div>
</div>
</div>
);
};
export default App
ON/OFFスイッチはヘッダーに表示します。
ON/OFFスイッチについては【React】ON/OFFスイッチを作るをご覧ください。
全体のCSSはstyle.cssに書きます。
:root {
--bg-color: #fff;
--text-color: #333;
--header-bg-color: #eee;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
.header {
background-color: var(--header-bg-color);
}
スイッチでダークモードに切り替える
スイッチONでサイトがダークモードに切り替わるようにします。
Appコンポーネント内に、スイッチをONまたはOFFにすると実行されるhandleChange関数を定義後、それをToggleSwitchButtonコンポーネントにpropsとして渡します。
また、handleChange
関数内でinput(type="checkbox")要素を使った処理を行うため、useRefでinput要素の参照checkboxElement
をApp.js内に定義し、それをpropsとしてToggleSwitchButtonコンポーネントに渡します。
App.jsを以下のように書き換えます。
// 省略
const App = () => {
const checkboxElement = useRef(null)
const handleChange = useCallback(e => {
const btn = e.target
const body = document.body
if(btn.checked === true) {
body.classList.add("dark-theme")
}else{
body.classList.remove("dark-theme")
}
})
return (
<div>
<header className="header">
<div>LOGO</div>
<ToggleSwitchButton
className="toggle-switch-button"
handleChange={handleChange}
ref={checkboxElement}
offColor="#ccc"
onColor="#383896"
size="1em"
/>
</header>
<div className="container">
<div>example</div>
</div>
</div>
);
};
export default App
handleChange関数では、スイッチをONにした(チェックボックスにチェックが入った)ときにbody要素にdark-themeクラスを追加して背景色、文字色、ヘッダーの色をダークモード仕様に変化させます。
if(btn.checked === true) {
body.classList.add("dark-theme")
}
dark-themeクラスはstyle.cssに記述します。
// 省略
body.dark-theme {
--bg-color: #333;
--text-color: #ddd;
--header-bg-color: #222;
}
反対に、スイッチをOFFにした(チェックボックスからチェックが外れた)ときは、body要素からdark-themeクラスを削除します。
else{
body.classList.remove("dark-theme")
}
これで、スイッチをONにするとサイトがダークモードに、OFFにすると通常モードに切り替わるようになりました。
ページ移動後等もダークモードONが維持されるようにする
今のままではダークモードに切り替えたあとに、ブラウザのリロードでページを更新したり、現在のページから離れてしまうと、次にサイトを訪れた際に通常モードにリセットされてしまいます。
これを解決するために、Cookieを利用してON/OFFの状態をブラウザに保存します。
これにより、ダークモードに切り替えたあとにページ移動しても、ダークモードが維持された状態になります。
それではやっていきます。まず、JSでcookieを簡単に操作できるようにするため、下記のコマンドでjs-cookieをインストールします。
yarn add js-cookie
そして、App.jsを以下のように書き換えます。
// 省略
const App = () => {
// 省略
const handleChange = useCallback(e => {
// 省略
if(btn.checked === true) {
body.classList.add("dark-theme")
Cookies.set('darkMode', 'on')
}else{
body.classList.remove("dark-theme")
Cookies.remove('darkMode')
}
})
useLayoutEffect(() => {
const darkModeCookie = Cookies.get('darkMode')
const body = document.body
const checkbox = checkboxElement.current
if(darkModeCookie){
body.classList.add('dark-theme');
checkbox.checked = true
}
}, [])
// 省略
};
変更点は、まずhandleChange関数です。
スイッチをONにしたときに、Cookie.setでdarkModeという名前のCookieがブラウザに保存します。darkModeには'on'という文字列の値を入れました。
if(btn.checked === true) {
body.classList.add("dark-theme")
Cookies.set('darkMode', 'on')
}
反対に、スイッチをOFFにしたときは、Cookies.removeでdarkModeという名前のCookieをブラウザから削除します。
else{
body.classList.remove("dark-theme")
Cookies.remove('darkMode')
}
これで、スイッチがON/OFFになる度にCookieが保存・削除されます。
もう一つの変更点は、useLayoutEffectフックを呼び出しているところです。useLayoutEffectフックは、Appコンポーネントがブラウザに表示される前に実行されます(Appコンポーネント内で呼び出しているため)。
呼び出されると、第1引数の関数を実行します。このとき、第2引数に空の配列を渡すと、Appコンポーネントがブラウザに表示される前に一度だけ第1引数の関数が実行されます。
つまり、ブラウザのリロードでページが更新されたときや、ページ移動したときに呼び出されます。
第1引数の関数では、Cookies.getでCookieを取得後、Cookieの有無を確認してCookieが保存されていれば、checkbox.checked = true
でスイッチをONにしたあとに、body要素にdark-themeクラスを追加してダークモード仕様のスタイルを適用します。
if(darkModeCookie){
body.classList.add('dark-theme');
checkbox.checked = true
}
以上で、ブラウザのリロード後やページ移動後もダークモードON/OFFの状態が維持されるようになりました。
OSのダークモードONに合わせてサイトのダークモードをONにする
スイッチの操作でサイトがダークモードに切り替わるようになりましたが、それ以外に、ユーザーがOSのダークモード(Window10でアプリをダークモードにするには、設定の「個人用設定」→「色」→「既定のアプリモードを選択します」でダークを選択します)をONにしたときに、それに合わせて自動的にサイトがダークモードに切り替わるようにします。
これを可能にするには、CSSの@media (prefers-color-scheme: dark) {...}
内にダークモード時に適用したいスタイルを記述します。
それでは、style.cssに以下のCSSを追加します。
/*
省略
*/
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #333;
--text-color: #fff;
--header-bg-color: #222;
}
}
/*
省略
*/
これで終わりではありません。
なぜなら、以下の問題点があるからです。
- サイトのスイッチはOFFのまま
- OSのダークモードONに合わせてサイトのダークモードがONになったあとに、サイトのスイッチでOFFにしてもサイトの色が戻らない
- OSのスイッチでONにしたあとにサイトのスイッチでOFFにして通常モード仕様のスタイルに戻した後に、OSのスイッチをOFF→ONにした場合にサイトの色が変わらない
- サイトのスイッチでONにしたあとにOSのスイッチをON→OFFにしてもサイトの色が通常モード仕様に戻らない点
サイトのスイッチはOFFのまま
OSのスイッチONでサイトの色はダークモード仕様に切り替わるようになりましたが、サイトのスイッチはOFFのまま動いてくれません。
CSSではスイッチをONの方向へ動かす(チェックを入れる)ことはできないため、JSで動かす必要があります。
その方法はCSSと同様に、JSでprefers-color-scheme: dark
でOSのスイッチON/OFFを検知して、ONであればサイトのスイッチをONにします。
App.jsに以下のコードを追加します。
const App = () => {
// 省略
useEffect(() => {
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const darkModeOn = darkModeMediaQuery.matches
const body = document.body
const checkbox = checkboxElement.current
darkModeMediaQuery.addListener((e) => {
const darkModeOn = e.matches;
if (darkModeOn) {
Cookies.set('darkMode', 'on')
checkbox.checked = true
} else {
Cookies.remove('darkMode')
checkbox.checked = false
}
});
}, [])
return (
// 省略
)
}
JSでprefers-color-scheme: dark
を使って検知するには、window.matchMedia('(prefers-color-scheme: dark)')
を使います。
checkbox.checked = true
にすると、ToggleSwitchButtonコンポーネントのCSSで定義した
& input {
/*省略*/
&:checked + label {
/*省略*/
&::before {
left: 2em;
}
}
}
の部分が適用されてスイッチがONの方向へ移動します。
また、この状態でページを離れると、次に訪れたときに状態がリセットされてしまうため、ここでもCookieを保存します。
if (darkModeOn) {
Cookies.set('darkMode', 'on')
checkbox.checked = true
}
OSのスイッチをOFFにした場合はサイトのスイッチをOFFにしてCookieを削除します。
else {
Cookies.remove('darkMode')
checkbox.checked = false
}
これでOSのダークモードONに合わせてサイトのダークモードがONに切り替わるようになりました。
OSのスイッチONに合わせてサイトのスイッチがONになったあとにサイトのスイッチでOFFにしてもサイトの色が戻らない
サイトのスイッチのON/OFFでは、CSSの@media (prefers-color-scheme: dark) {...}
は適用・解除されないからです。
適用・解除されるのはOSのスイッチでON/OFFにしたときだけです。
そういった理由から、今のままではOSのスイッチでOFFにしない限り、サイトを通常モード仕様のスタイルには戻せないのです。
この問題を解決するには、サイトのスイッチがOFFになったタイミングでbody要素に通常モード仕様のスタイルを持つクラスを追加します。
それではまず、style.cssにそのクラスを定義します。クラス名はlight-themeとします。
/*
省略
*/
body.light-theme {
--bg-color: #fff;
--text-color: #333;
--header-bg-color: #eee;
}
そして、サイトのスイッチをOFFにしたタイミングでbody要素にlight-themeクラスを追加します。
handleChange関数を以下のように書き換えます。
const handleChange = useCallback(e => {
const btn = e.target
const body = document.body
if(btn.checked === true) {
// 省略
}else{
body.classList.remove("dark-theme")
body.classList.add("light-theme")
Cookies.remove('darkMode')
}
})
しかし、これではまだ不十分です。ここからサイトのスイッチでONにしたときにhandleChange関数によってdark-themeが追加されますが、先ほどのlight-themeクラスも追加されているため、light-themeクラスのスタイルが優先されてしまい、ダークモード仕様に変化してくれません。
なので、サイトのスイッチでONにしたタイミングでbody要素からlight-themeクラスを削除します。
handleChange関数を以下のように書き換えます。
const handleChange = useCallback(e => {
const btn = e.target
const body = document.body
if(btn.checked === true) {
if(btn.checked === true) {
body.classList.remove("light-theme")
body.classList.add("dark-theme")
Cookies.set('darkMode', 'on')
}else{
// 省略
}
}
})
これで、OSのスイッチでONにしたあとにサイトのスイッチでOFFにしたときに通常モード仕様のスタイルに戻るようになりました。
サイトのスイッチでOFFにしたあとにOSのスイッチをOFF→ONにしてもサイトの色がダークモード仕様に変わらない
OSのスイッチでONにしたあとにサイトのスイッチでOFFにした場合に通常モード仕様のスタイルに戻るようにはなりましたが、その後にOSのスイッチをOFF→ONにした場合にサイトの色がダークモード仕様に切り替わらないという問題が起こります。これはサイトのスイッチでOFFにしたときにlight-themeクラスが追加されたため、CSSで指定した@media (prefers-color-scheme: dark) {...}
のスタイルが効かないためです。
なので、OSのスイッチがOFF→ONになったタイミングでbody要素からlight-themeクラスを削除します。
App.jsの以下の箇所を書き換えます。
const App = () => {
// 省略
useEffect(() => {
// 省略
darkModeMediaQuery.addListener((e) => {
const darkModeOn = e.matches;
if (darkModeOn) {
body.classList.remove('light-theme');
checkbox.checked = true
} else {
// 省略
}
});
}, [])
return (
// 省略
)
}
これでこの問題は解決しました。
サイトのスイッチでONにしたあとにOSのスイッチをON→OFFにしてもサイトの色が通常モード仕様に戻らない点
OSのスイッチがON→OFFになったタイミングでbody要素からdark-themeクラスを削除します。
App.jsの以下の箇所を書き換えます。
const App = () => {
// 省略
useEffect(() => {
// 省略
darkModeMediaQuery.addListener((e) => {
const darkModeOn = e.matches;
if (darkModeOn) {
// 省略
} else {
body.classList.remove('dark-theme');
Cookies.remove('darkMode')
checkbox.checked = false
}
});
}, [])
return (
// 省略
)
}
これでこの問題は解決しました。
カスタムフックを作成する
サイトにダークモード機能を実装することはできましたが、Appコンポーネント内にhandleChange関数やuseEffectフック等を書いたことで、Appコンポーネントが肥大化してしまいました。
こんなんではコードの見通しが悪いため、Appコンポーネントからロジックを切り離し、それをカスタムフックとして関数でまとめたあとにそれをAppコンポーネント内で呼び出すようにします。
カスタムフックとは、簡単にいうとコンポーネントのロジックをほかのコンポーネントでも再利用できるように関数にしたものです。また今回のように、ただコンポーネントを見やすくするためだけに使われることもあります。
それでは、useDarkModeButton.jsを作成し、そこにuseDarkModeButtonという名前のカスタムフックを定義します。
import {
useCallback,
useRef,
useEffect,
useLayoutEffect,
} from 'react'
const useDarkModeButton = () => {
const checkboxElement = useRef(null)
const handleChange = useCallback(e => {
const btn = e.target
const body = document.body
if(btn.checked === true) {
body.classList.remove("light-theme")
body.classList.add("dark-theme")
Cookies.set('darkMode', 'on')
}else{
body.classList.remove("dark-theme")
body.classList.add("light-theme")
Cookies.remove('darkMode')
}
})
useLayoutEffect(() => {
const darkModeCookie = Cookies.get('darkMode')
const body = document.body
const checkbox = checkboxElement.current
if(darkModeCookie){
//body.classList.remove('light-theme');
body.classList.add('dark-theme');
checkbox.checked = true
}else{
//body.classList.remove('dark-theme');
//body.classList.add('light-theme');
//checkbox.checked = false
}
}, [])
useEffect(() => {
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const darkModeOn = darkModeMediaQuery.matches
const body = document.body
const checkbox = checkboxElement.current
darkModeMediaQuery.addListener((e) => {
const darkModeOn = e.matches;
if (darkModeOn) {
body.classList.remove('light-theme');
//body.classList.add('dark-theme');
Cookies.set('darkMode', 'on')
checkbox.checked = true
} else {
body.classList.remove('dark-theme');
//body.classList.add('light-theme');
Cookies.remove('darkMode')
checkbox.checked = false
}
});
}, [])
return [
checkboxElement,
handleChange
]
}
export default useDarkModeButton
そして、Appコンポーネント内でuseDarkModeButtonフックを呼び出します。
const App = () => {
const [
checkboxElement,
handleChangeDarkMode
] = useDarkModeButton()
return (
<div>
<header className="header">
<div>LOGO</div>
<ToggleSwitchButton
className="toggle-switch-button"
handleChange={handleChangeDarkMode}
ref={checkboxElement}
offColor="#ccc"
onColor="#383896"
size="1em"
/>
</header>
<div className="container">
<div>example</div>
</div>
</div>
);
};
これでAppコンポーネントの見通しが良くなりました。
以上です。
Demo
下記で実際の動作を確認できます。