【Next.js】next-themesでライトモードとダークモードを切り替える|簡単なToDoアプリの作成

Next.jsプロジェクトに、`next-themes`ライブラリを使ってライトモードとダークモードの切り替え機能を実装する手順を解説します。Tailwind CSSと連携し、テーマ切り替えボタンの作成からアプリ全体への適用方法まで、初心者向けにわかりやすく説明します。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • node: 22.14.0
  • react: 19.0.0
  • next: 15.3.2
  • tailwindcss: 4
  • Prisma: 6.8.2
  • NextAuth: 5.8.0
  • @auth/prisma-adapter: 2.9.1
  • @prisma/client: 6.8.2
  • bcrypt: 6.0.0
  • @types/bcrypt: 5.0.2
  • zod: 3.25.56
  • nodemailer: 6.10.1
  • @types/nodemailer: 6.4.17
  • next-themes: 0.4.6

サンプルコード

/package.json

/package.json
1     "bcrypt": "^6.0.0",
2     "next": "15.3.2",
3     "next-auth": "^4.24.11",
4+    "next-themes": "^0.4.6",
5     "nodemailer": "^6.10.1",
6     "prisma": "^6.8.2",
7     "react": "^19.0.0",
8

/src/app/components/Header.tsx

/src/app/components/Header.tsx
1 import { useState } from 'react'
2 import Link from 'next/link'
3 import { useSession, signOut } from 'next-auth/react'
4+import ThemeToggle from './ThemeToggle';
5 
6 export default function Header() {
7     const [menuOpen, setMenuOpen] = useState(false)
8     const { data: session, status } = useSession()
9 
10     return (
11-        <header className="bg-white shadow-md">
12+        <header className="bg-white dark:bg-black shadow-md">
13             <div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
14                 {/* ロゴ */}
15                 <Link href="/">
16                 </button>
17 
18                 {/* メニュー(PC用) */}
19-                <nav className="hidden md:flex items-center space-x-4 md:space-x-6">
20+                <nav className="hidden md:flex items-center space-x-4 md:space-x-6 text-gray-700 dark:text-white">
21                     {status === 'loading' ? (
22                         <span className="ml-4">Loading...</span>
23                     ) : session ? (
24                     <Link href="/contact" className="hover:underline cursor-pointer">
25                         お問い合わせ
26                     </Link>
27+                    <ThemeToggle />
28                 </nav>
29             </div>
30 
31             {/* メニュー(スマホ用) */}
32             {menuOpen && (
33-                <nav className="md:hidden px-4 pb-4 space-y-2 border-t border-gray-300 text-gray-700 font-medium">
34+                <nav className="md:hidden px-4 pb-4 space-y-2 border-t border-gray-300 text-gray-700 dark:text-white font-medium">
35                     {status === 'loading' ? (
36                         <span className="block py-2">Loading...</span>
37                     ) : session ? (
38                     >
39                         お問い合わせ
40                     </Link>
41+                    <ThemeToggle />
42                 </nav>
43             )}
44         </header>
45

/src/app/components/ThemeToggle.tsx

/src/app/components/ThemeToggle.tsx
1+'use client';
2+
3+import { useTheme } from 'next-themes';
4+import { useEffect, useState } from 'react';
5+
6+export default function ThemeToggle() {
7+  const { theme, setTheme, resolvedTheme } = useTheme();
8+  const [mounted, setMounted] = useState(false);
9+
10+  useEffect(() => setMounted(true), []);
11+
12+  if (!mounted) return null;
13+
14+  return (
15+    <button
16+      onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
17+      className="px-3 py-1 text-sm rounded border hover:bg-gray-200 dark:hover:bg-gray-700"
18+      aria-label="テーマ切り替え"
19+    >
20+      {resolvedTheme === 'dark' ? 'ライトモード' : 'ダークモード'}
21+    </button>
22+  );
23+}
24

/src/app/globals.css

/src/app/globals.css
1 @import "tailwindcss";
2+@custom-variant dark (&:where(.dark, .dark *));
3 
4 :root {
5   --background: #ffffff;
6

/src/app/layout.tsx

/src/app/layout.tsx
1 import Footer from '@/app/components/Footer'
2 import FlashMessage from '@/app/components/FlashMessage'
3 import { FlashMessageProvider } from "@/app/context/FlashMessageContext"
4-
5+import { ThemeProvider } from 'next-themes';
6 
7 const geistSans = Geist({
8   variable: "--font-geist-sans",
9   children: React.ReactNode;
10 }>) {
11   return (
12-    <html lang="en">
13+    <html lang="en" suppressHydrationWarning>
14       <body
15         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
16       >
17-        <FlashMessageProvider>
18-          <SessionWrapper>
19-            <Header />
20-            <FlashMessage />
21-            {children}
22-            <Footer />
23-          </SessionWrapper>
24-        </FlashMessageProvider>
25+        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
26+          <FlashMessageProvider>
27+            <SessionWrapper>
28+                <Header />
29+                <FlashMessage />
30+                {children}
31+                <Footer />
32+            </SessionWrapper>
33+          </FlashMessageProvider>
34+        </ThemeProvider>
35       </body>
36     </html>
37   )
38

コード解説

変更点: next-themesライブラリのインストール

/package.json
1     "next": "15.3.2",
2     "next-auth": "^4.24.11",
3+    "next-themes": "^0.4.6",
4     "prisma": "^6.8.2",
5     "react": "^19.0.0",

Next.jsプロジェクトに、ライトモードとダークモードのテーマ切り替え機能を簡単に追加するためのライブラリnext-themesをインストールします。このライブラリをプロジェクトの依存関係に追加することで、テーマの状態管理や切り替えロジックを自分で実装する手間を省くことができます。

変更点: アプリ全体へのテーマ機能の適用

/src/app/layout.tsx
1+import { ThemeProvider } from 'next-themes';
2 
3 export default function RootLayout({
4   children,
5 }: {
6   children: React.ReactNode;
7 }>) {
8   return (
9-    <html lang="en">
10+    <html lang="en" suppressHydrationWarning>
11       <body
12         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
13       >
14-        <FlashMessageProvider>
15-          <SessionWrapper>
16-            <Header />
17-            <FlashMessage />
18-            {children}
19-            <Footer />
20-          </SessionWrapper>
21-        </FlashMessageProvider>
22+        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
23+          <FlashMessageProvider>
24+            <SessionWrapper>
25+                <Header />
26+                <FlashMessage />
27+                {children}
28+                <Footer />
29+            </SessionWrapper>
30+          </FlashMessageProvider>
31+        </ThemeProvider>
32       </body>
33     </html>
34   )

インストールしたnext-themesライブラリのThemeProviderコンポーネントをインポートし、アプリケーション全体を囲みます。これにより、サイト内のどのページでもテーマ情報を利用できるようになります。

  • attribute="class": テーマが変更されたときに、HTMLの<html>タグにクラス名(例: class="dark")を追加する設定です。これにより、Tailwind CSSのdark:プレフィックスを使ってスタイルを適用できるようになります。
  • defaultTheme="system": ユーザーが使用しているOSのテーマ設定(ライトモードかダークモードか)を、Webサイトの初期テーマとして採用します。
  • enableSystem: OSのテーマ設定との連携を有効にします。
  • suppressHydrationWarning: サーバーサイドで描画されるHTMLとクライアントサイドで描画されるHTMLでテーマのクラス名が異なると警告が表示されることがあります。これを抑制するための設定で、next-themesの使用時に推奨されています。

変更点: Tailwind CSSでのダークモード設定

/src/app/globals.css
1 @import "tailwindcss";
2+@custom-variant dark (&:where(.dark, .dark *));
3 
4 :root {
5   --background: #ffffff;

Tailwind CSSでダークモードを有効にするための設定を追加します。この記述により、<html>タグにdarkクラスが付与された場合に、dark:というプレフィックスを持つスタイル(例: dark:bg-black)が適用されるようになります。これは、先ほどのlayout.tsxで行ったattribute="class"の設定と連携して機能します。

変更点: テーマ切り替えボタンコンポーネントの作成

/src/app/components/ThemeToggle.tsx
1+'use client';
2+
3+import { useTheme } from 'next-themes';
4+import { useEffect, useState } from 'react';
5+
6+export default function ThemeToggle() {
7+  const { theme, setTheme, resolvedTheme } = useTheme();
8+  const [mounted, setMounted] = useState(false);
9+
10+  useEffect(() => setMounted(true), []);
11+
12+  if (!mounted) return null;
13+
14+  return (
15+    <button
16+      onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
17+      className="px-3 py-1 text-sm rounded border hover:bg-gray-200 dark:hover:bg-gray-700"
18+      aria-label="テーマ切り替え"
19+    >
20+      {resolvedTheme === 'dark' ? 'ライトモード' : 'ダークモード'}
21+    </button>
22+  );
23+}

テーマを切り替えるためのボタンコンポーネントを新規に作成します。

  • 'use client': このコンポーネントがブラウザ上で動作するクライアントコンポーネントであることを宣言します。useStateuseEffectといったReactのフックや、ユーザーの操作に応答する機能を使うために必要です。
  • useTheme(): next-themesが提供するフックです。現在のテーマ情報を取得したり、テーマを変更したりするための便利な関数(setTheme)を受け取ることができます。resolvedThemeは、テーマがsystemに設定されている場合に、実際に表示されているテーマ('light''dark')を取得します。
  • useEffectuseState: サーバーとクライアントで表示内容が一致しないことによるエラーを防ぐための記述です。コンポーネントがブラウザ上で完全に読み込まれてから(マウントされてから)ボタンを表示するようにしています。
  • onClickイベント: ボタンがクリックされたときに、現在のテーマがdarkならlightに、そうでなければdarkにテーマを切り替えます。
  • 表示テキスト: 現在のテーマに応じて、ボタンのテキストを「ライトモード」または「ダークモード」に動的に変更します。

変更点: ヘッダーへのテーマ切り替えボタンの配置とスタイル適用

/src/app/components/Header.tsx
1 import Link from 'next/link'
2 import { useSession, signOut } from 'next-auth/react'
3+import ThemeToggle from './ThemeToggle';
4 
5 export default function Header() {
6     const [menuOpen, setMenuOpen] = useState(false)
7 
8     return (
9-        <header className="bg-white shadow-md">
10+        <header className="bg-white dark:bg-black shadow-md">
11             <div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
12...
13                 {/* メニュー(PC用) */}
14-                <nav className="hidden md:flex items-center space-x-4 md:space-x-6">
15+                <nav className="hidden md:flex items-center space-x-4 md:space-x-6 text-gray-700 dark:text-white">
16...
17                     <Link href="/contact" className="hover:underline cursor-pointer">
18                         お問い合わせ
19                     </Link>
20+                    <ThemeToggle />
21                 </nav>
22             </div>
23 
24             {/* メニュー(スマホ用) */}
25             {menuOpen && (
26-                <nav className="md:hidden px-4 pb-4 space-y-2 border-t border-gray-300 text-gray-700 font-medium">
27+                <nav className="md:hidden px-4 pb-4 space-y-2 border-t border-gray-300 text-gray-700 dark:text-white font-medium">
28...
29                     >
30                         お問い合わせ
31                     </Link
32+                    <ThemeToggle />
33                 </nav>
34             )}
35         </header>

作成したThemeToggleコンポーネントをヘッダーに配置し、ヘッダー自体もダークモードに対応させます。

  • ThemeToggleのインポートと配置: 作成したThemeToggle.tsxをインポートし、PC用とスマートフォン用のナビゲーションメニュー内にそれぞれ配置します。
  • ダークモード用スタイルの追加: headerタグやnavタグに、dark:bg-blackdark:text-whiteといったTailwind CSSのクラスを追加します。これにより、テーマがダークモードに切り替わると、ヘッダーの背景色が黒に、文字色が白に自動的に変更されるようになります。

おわりに

この記事では、next-themesライブラリを使って、Next.jsアプリケーションにダークモード切り替え機能を実装する手順を解説しました。layout.tsxでアプリ全体をThemeProviderで囲み、attribute="class"を指定することが、Tailwind CSSのdark:プレフィックスを有効にする鍵となります。また、useThemeフックを利用することで、現在のテーマを取得し切り替えるためのボタンコンポーネントを非常にシンプルに作成できました。これらの手順だけで、ユーザーのOS設定とも連携する本格的なテーマ機能を、手軽に実装できることがお分かりいただけたかと思います。

関連コンテンツ