Docs / tech

Next.js と Tailwind CSS でダークモードの実装

2025-11-23 |

サイトを実装するときに、企業向けはあまり利用されませんが、それ以外に多いダークモードの実装を、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 ファイルとして以下のファイルを作成します。

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行目のコードを追加します。

app/globals.css
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

これでコンポーネントの準備ができました。

ページにアイコンを実装

本来はナビゲーションにボタンを配置するのが正しい形ですが、今回は動作確認だけのため、pages.tsx にコードを追加します。main の前にコンポーネントを配置して、また作成したコンポーネントをインポートします。

app/page.tsx
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

以下のようにボタンを押下すると、モードが切り替わるようになりました。

まとめ

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

サンプルのコードに関しては、以下のリポジトリに公開をしています。