Next.js - ダークモードの切り替えボタンを実装する方法
1. はじめに
以前このブログのWebサイトをダークモードに対応させる方法という記事で紹介したコードをNext.jsで使えるようにしてみました。
2. Next.jsで"window is not defined"を回避する方法
ダークモードの実装でwindow.matchMedia
などを使用したいときにそのまま書くとエラーになってしまいます。それを回避する方法が何種類かあるので忘れないように書いておこうと思います。詳しい内容はコチラの記事がわかりやすいと思います。
typeof window
if (typeof window !== "undefined") {
//...
}
useEffect
import { useEffect } from "react";
useEffect(() => {
//....
}, [])
dynamic import
import dynamic from "next/dynamic";
const XXXX = dynamic(() => {
return import("../components/FileName");
},
{ ssr: false }
);
<XXXX />
3. ダークモードの実装
はじめにtheme.tsx
というファイルを作成します。(ファイル名はTypeScriptですがTypeScript化してないのでjsでもok)。上記の「"window is not defined"を回避する方法」は、ここではif (typeof window !== "undefined") {...}
を使用しています。下記のざっくりした内容は、ThemeProvider
で<html>
タグにカスタムデータ属性の追加とlocalStorage
にテーマを保存するようにしてtoggleTheme
でその切替をするようにしています。
//theme.tsx
import { createContext, useContext } from 'react'
const toggleTheme = () => {
if (typeof window !== 'undefined') {
let root = document.documentElement
let mql = window.matchMedia('(prefers-color-scheme: dark)')
let storage = localStorage
let getTheme = storage.getItem('theme')
if (getTheme === 'dark') {
root.setAttribute('data-theme', 'light')
storage.setItem('theme', 'light')
} else if (getTheme === 'light') {
root.setAttribute('data-theme', 'dark')
storage.setItem('theme', 'dark')
} else if (mql.matches) {
root.setAttribute('data-theme', 'light')
storage.setItem('theme', 'light')
} else {
root.setAttribute('data-theme', 'dark')
storage.setItem('theme', 'dark')
}
}
}
const ThemeContext = createContext(() => {})
export const ThemeProvider = ({ children }) => {
if (typeof window !== 'undefined') {
let root = document.documentElement
let mql = window.matchMedia('(prefers-color-scheme: dark)')
let storage = localStorage
let getTheme = storage.getItem('theme')
const mediaTheme = (mql) => {
if (mql.matches) {
root.setAttribute('data-theme', 'dark')
storage.setItem('theme', 'dark')
} else {
root.setAttribute('data-theme', 'light')
storage.setItem('theme', 'light')
}
}
mql.addListener(mediaTheme)
if (getTheme === 'dark') {
root.setAttribute('data-theme', 'dark')
storage.setItem('theme', 'dark')
} else if (getTheme === 'light') {
root.setAttribute('data-theme', 'light')
storage.setItem('theme', 'light')
}
}
return (
<ThemeContext.Provider value={toggleTheme}>
{children}
</ThemeContext.Provider>
)
}
export const useToggleTheme = () => useContext(ThemeContext)
上記で作成したtheme.tsx
のThemeProvider
を_app.tsx
に追加します。
//_app.js
import '../styles/globals.scss' //cssでもok
import { ThemeProvider } from '../components/theme'
import { AppProps } from 'next/app'
const App = ({ Component, pageProps }: AppProps) => {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
)
}
export default App
CSSでダークモードを実装する場合、通常は@media (prefers-color-scheme: dark) {...}
のように書きますが、ここではカスタムデータ属性で切り替えを行っているので書かなくて大丈夫です。
/*globals.scss*/
[data-theme='dark'] {
--bg-color: #333;
--text-color: #fff;
}
body {
background-color: var(--bg-color);
}
h1 {
color: var(--text-color);
}
最後に切り替えボタンを出力したい場所に下記のようにボタンを追加します。
//index.tsx
import { useToggleTheme } from '../components/theme'
const Home = () => {
const toggle = useToggleTheme()
return (
<div>
<h1>Theme</h1>
<button onClick={toggle}>Toggle</button>
</div>
)
}
export default Home
data-* - HTML: HyperText Markup Language | MDN
フック API リファレンス – React