Next.js と Tailwind CSS でダークモードの実装
サイトを実装するときに、企業向けはあまり利用されませんが、それ以外に多いダークモードの実装を、Next.js と Tailwind CSS を利用して実装します。
Next.js のプロジェクトを作成する
まず最初に、Next.js のプロジェクトを作成します。
npx create-next-app
途中、プロジェクトの名前の確認、またデフォルトの設定でよいか?という確認が出てきます。デフォルトで Tailwind CSS が適用されるので、今回は Yes で進めていきます。
Need to install the following packages:
[email protected]
Ok to proceed? (y)
✔ What is your project named? … nextjs-darkmode
✔ Would you like to use the recommended Next.js defaults? › Yes, use recommended defaults
Creating a new Next.js app in /home/haramizu/github/nextjs-darkmode.
作成されたらプロジェクトを実行します。
cd nextjs-darkmode
npm run dev
無事、実行できました。

アイコンライブラリを追加
モードを切り替えるボタンをコンポーネントとして実装します。このコンポーネントで利用するアイコンとしては、今回は Lucide React を利用します。
以下のコマンドを実行します。
npm install lucide-react
これでアイコンをサイトで使えるようになりました。
コンポーネントの追加
続いてコンポーネントを追加します。components フォルダを追加して、ThemeSwitcher.tsx ファイルとして以下のファイルを作成します。
"use client";
import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";
export default function ThemeSwitcher() {
const [mounted, setMounted] = useState(false);
const [isDark, setIsDark] = useState(false);
// Set mounted to true after first render to avoid hydration mismatch
useEffect(() => {
// Get initial theme
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null;
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const shouldBeDark = savedTheme === "dark" || (!savedTheme && systemPrefersDark);
// Update DOM
document.documentElement.classList.toggle("dark", shouldBeDark);
// Use setTimeout to avoid cascading render warning
setTimeout(() => {
setIsDark(shouldBeDark);
setMounted(true);
}, 0);
}, []);
const toggleTheme = () => {
const newIsDark = !isDark;
setIsDark(newIsDark);
localStorage.setItem("theme", newIsDark ? "dark" : "light");
document.documentElement.classList.toggle("dark", newIsDark);
};
// Avoid hydration mismatch by not rendering until client-side
if (!mounted) {
return (
<button
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800"
aria-label="Toggle theme"
disabled
>
<div className="w-5 h-5" />
</button>
);
}
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle theme"
>
{isDark ? (
<Sun className="w-5 h-5 text-yellow-500" />
) : (
<Moon className="w-5 h-5 text-gray-700" />
)}
</button>
);
}
続いてスタイルシートで darkmode を使えるようにするために、globals.css に以下のサンプルの2行目のコードを追加します。
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
これでコンポーネントの準備ができました。
ページにアイコンを実装
本来はナビゲーションにボタンを配置するのが正しい形ですが、今回は動作確認だけのため、pages.tsx にコードを追加します。main の前にコンポーネントを配置して、また作成したコンポーネントをインポートします。
import ThemeSwitcher from "@/components/ThemeSwitcher";
import Image from "next/image";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<header>
<ThemeSwitcher />
</header>
<main ...
これでボタンを配置することができました。実行します。
npm run dev
以下のようにボタンを押下すると、モードが切り替わるようになりました。

まとめ
ダークモードの実装に関して、特にむつかしい手順が割るわけでもなく、公式にもサンプルがあるのでハードルは低いですが、サイトを作るときに毎回作業をするので汎用的なコンポーネントを作っておくのを目的として、今回の記事をまとめました。
サンプルのコードに関しては、以下のリポジトリに公開をしています。