フロントエンドエンジニアの上垣です。
この記事では、Next.js で、next-themes と Tailwind CSS を使って Light/Dark モード切り替え対応する例を紹介しています。
前提
この記事では下記のバージョンのライブラリを利用しています。
- Next.js 13.4.12
- next-themes 0.2.1
- Tailwind CSS 3.3.2
Next.js の AppRouter を利用しています。
- この記事で紹介するのは、「Light/Dark モードの切り替え」であり、Dark モード実装の詳細には触れていません。
- サンプルアプリケーションは CodeSandbox で確認できます。
Tailwind CSS のダークモード
まず、Tailwind CSS のダークモード について簡単にまとめてみます。
Light/Dark モードの判定
Tailwindは、デフォルトではユーザーのシステムの prefers-color-scheme メディア特性の値を参照して、Light/Dark を判定します。prefers-color-scheme のみを参照する場合は、追加で特に何も設定する必要はないです。
一方、ユーザーの手動で Light/Dark モードを切り替えたい場合は、tailwind.config.js
に、下記を設定します。
/** @type {import('tailwindcss').Config} */ module.exports = { darkMode: 'class', // ... }
これにより、 dark
class が適用されている要素以下の要素に、Tailwind の dark モードのスタイルを適用できます。
Dark モードのスタイル
dark モードのスタイルは基本的に、既存のクラス名に dark:
prefix をつければ OK です。
<html class="dark dark:bg-black dark:text-white"> // dark モード用の class を定義 <body> <h1>Headline</h1> </body> </html>
next-themes
Next.js アプリケーションで Tailwind CSS の Light/Dark モード切り替えを組み込む場合、リクエスト毎に以下を実行する必要があります。
- 現在のモードを判定する
- HTML に現在のモードを反映する
- ユーザーが選択したモードを永続化(e.g. localStorage)する。
この一連の処理をうまく隠蔽して解決してくれるのが、next-themes です。
インストール
npm install next-themes
セットアップ
まず、next-themes が提供する ThemeProvider
をセットアップします。
// app/providers.tsx "use client"; import { FC, PropsWithChildren } from "react"; import { ThemeProvider } from "next-themes"; export const Providers: FC<PropsWithChildren> = ({ children }) => { return <ThemeProvider attribute="class">{children}</ThemeProvider>; };
ThemeProvider
は、システムの prefers-color-scheme
や、ブラウザの localStorage
へアクセスするため、クライアントサイドでの実行が必要です。そのため、use client
ディレクティブを付与した app/providers.tsx
でラップしています。
また、TemeProvider
に, attribute=”class”
を指定しています。これは前述したように、 Tailwind CSS では className で Light/Dark を判定する必要があるので、Tailwind と組み合わせる場合は必須です。
次に、app/providers.tsx
を layout.tsx
で読み込みます。
// app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html suppressHydrationWarning> <body className="min-h-screen dark:bg-black dark:text-white"> <Providers>{children}</Providers> </body> </html> ); }
html 要素に suppressHydrationWarning
を指定しているのは、next-themes がクライアントサイドで html 要素の className を書き換えることで発生する、ハイドレーション不一致の警告を抑制するためです。(参考)
なお、この警告抑制は1階層のみで機能するので、他の要素では警告を抑制しません。(ただ、suppressHydrationWarning
が理想的な解決策ではないことは、Cookie の利用も含めて issue でも議論されており、今後改善される可能性もあります。)
実行結果
この状態でアプリケーションを起動すると、下記が確認できます。
クライアントサイドの html 要素に、現在のモードが付与される
ローカルストレージに、現在のモードが永続化される
body タグの直下に、現在のモードを判定する 即時実行 function が挿入される
Light/Dark モードの切り替え
最後に、Light/Dark モードを切り替える select を作成します。
これも同様にクライアントサイドでの実行が必要なため、use client
ディレクティブを付与しています。サーバーサイドでは現在の theme を読み取れないため、クライアントでの mount を待ってから select を描画することで、ハイドレーションのミスマッチを抑制しています。
// themeSwitch.tsx "use client"; import { useState, useEffect } from "react"; import { useTheme } from "next-themes"; export const ThemeSwitch = () => { const [mounted, setMounted] = useState(false); const { theme, setTheme } = useTheme(); useEffect(() => { setMounted(true); }, []); if (!mounted) { return null; } return ( <select value={theme} onChange={(e) => setTheme(e.target.value)} className="border-2 border-black dark:border-white" > <option value="system">System</option> <option value="dark">Dark</option> <option value="light">Light</option> </select> ); };
サンプルアプリケーション
以上を実装したアプリケーションはこちらです。
モードを選択してからリロードしても、画面のチラつきなしで そのモードが適用されるのが確認できます。
感想
next-themes と Tailwind CSS で、ダークモード対応する例を紹介しました。
Light/Dark 切り替えを自前で実装するにしても、AppRouter では サーバー/クライアントの切り分けなど考慮することが多いので、next-themes のようなライブラリの存在は非常にありがたいです。
Tailwind CSS のダークモード実装詳細は、基本的にしっかりコンポーネント分割されていればそこまで大変ではないかなと思いつつ、より良いスタイルの管理方法もある気がするので、色々試しながら理解を深めていきたいと思います。