next-themes パッケージを使うと、かんたんに Next.js で作成されたアプリにダークモードを実装することができます。この記事では、現在最新バージョンの Next.js 13 でダークモードを実装します。
現在(2023, 4, 4) Next.js v13.2.4 app ディレクトリ(ベータ版)内で next-themes を使用すると、ハイドレーションエラーが表示されてしまいます。suppressHydrationWarning プロパティを使用すると、それを回避することができるようです。 参考記事 v13 でも、pages ディレクトリを使用すると、エラーは表示されません。
アプリの作成・ファイル構成
アプリを作成
npx create-next-app@latest
next-themes をインストール
npm install next-themes
# or
yarn add next-themes
ファイル構成
app //
|-- components //
| |-- Providers.js
| `-- ThemeButton.js
|-- globals.css
|-- layout.js
`-- page.js
app ディレクトリ内の components ディレクトリに、 Providers.js と ThemeButton.js ファイルを追加し、元からある globals.css と layout.js と page.js ファイルを変更していきます。
next-themes を使う準備
export default function Home() {
return (
<main>
</main>
)
}
初期画面を構成するために色々書かれているので、今回必要ないので、消しときます。
:root {
--background: white;
--foreground: black;
}
[data-theme='dark'] {
--background: black;
--foreground: white;
}
body {
background: var(--background);
color: var(--foreground);
}
親のタグに data-theme=”dark” の有無で背景と文字色が切り替わるようにします。
next-themes では、html タグに data-theme 属性に dark または、light の値を付与してダークモードとライトモードを切り替えます。
オプションを使用して、属性ではなく、クラスを使用することもできます。
next-themes でダークモードを実装する
import Providers from "./components/Providers";
import ThemeButton from "./components/ThemeButton";
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>
<ThemeButton />
{children}
</Providers>
</body>
</html>
);
}
1, 14, 17: Providers コンポーネント
2, 15: ThemeButton コンポーネントを 読み込みます。
12 : suppressHydrationWarning を追加して、エラーを非表示にする。
suppressHydrationWarning={true}
を追加することで警告の発生を停止させることが可能です。それは 1 階層下の要素までで機能するものであり、また避難ハッチとして使われるものです。そのため、多用しないでください。テキストコンテンツでない限り、React は修復を試行しようとはしないため、将来の更新まで不整合が残る可能性があります。 react.js.org
エラーを解決する場合
"use client";
import React, { useEffect, useState } from "react";
import { ThemeProvider } from "next-themes";
export default function Providers({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<>
<ThemeProvider defaultTheme="system" disableTransitionOnChange="false" enableSystem >{children}</ThemeProvider>
</>
);
}
ハイドレーションエラーを解決するには、ページがクライアントにマウントされてからレンダリングされるようにします。しかし、画面読み込み時にちらつきが発生してしまいます。
Providers.js
"use client";
import React from "react";
import { ThemeProvider } from "next-themes";
export default function Providers({ children }) {
return (
<>
<ThemeProvider defaultTheme="system" enableSystem >{children}</ThemeProvider>
</>
);
}
3, 8: ThemeProvider コンポーネントで囲み、プロパティを追加して設定の変更をすることができます。
next-themes
テーマ切り替えボタン
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function ThemeButton() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="system">System</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
);
}
4,7,9-11,13: ハイドレーションエラー対策
6: theme の値を使って、テーマ切り替えボタンの見た目、setTheme にテーマの文字列を渡して、テーマの切り替えをすることができます。