Darkモード実装(Tailwind CSS)
Next.js/ReactでのDarkモード実装(Tailwind CSS)
Section titled “Next.js/ReactでのDarkモード実装(Tailwind CSS)”Tailwind CSSを使用して、Next.jsやReactアプリにdarkモード切り替え機能を実装する方法を詳しく解説します。
1. Tailwind CSSの設定
Section titled “1. Tailwind CSSの設定”tailwind.config.jsの設定
Section titled “tailwind.config.jsの設定”/** @type {import('tailwindcss').Config} */module.exports = { darkMode: 'class', // 'media' または 'class' を指定 content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: {}, }, plugins: [],}設定の違い:
'media': システムの設定(prefers-color-scheme)に自動的に従う'class': HTML要素のclass属性で手動制御(推奨)
2. Next.jsでの実装(App Router)
Section titled “2. Next.jsでの実装(App Router)”ThemeProviderの作成
Section titled “ThemeProviderの作成”'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = { children: React.ReactNode defaultTheme?: Theme storageKey?: string}
type ThemeProviderState = { theme: Theme setTheme: (theme: Theme) => void}
const initialState: ThemeProviderState = { theme: 'system', setTheme: () => null,}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({ children, defaultTheme = 'system', storageKey = 'vite-ui-theme', ...props}: ThemeProviderProps) { const [theme, setTheme] = useState<Theme>( () => (typeof window !== 'undefined' && localStorage.getItem(storageKey)) as Theme || defaultTheme )
useEffect(() => { const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') { const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') .matches ? 'dark' : 'light'
root.classList.add(systemTheme) return }
root.classList.add(theme) }, [theme])
const value = { theme, setTheme: (theme: Theme) => { localStorage.setItem(storageKey, theme) setTheme(theme) }, }
return ( <ThemeProviderContext.Provider {...props} value={value}> {children} </ThemeProviderContext.Provider> )}
export const useTheme = () => { const context = useContext(ThemeProviderContext)
if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider')
return context}layout.tsxでの設定
Section titled “layout.tsxでの設定”import { ThemeProvider } from './providers/theme-provider'
export default function RootLayout({ children,}: { children: React.ReactNode}) { return ( <html lang="ja" suppressHydrationWarning> <body> <ThemeProvider defaultTheme="system" storageKey="app-theme" > {children} </ThemeProvider> </body> </html> )}テーマ切り替えコンポーネント
Section titled “テーマ切り替えコンポーネント”'use client'
import { useTheme } from '@/app/providers/theme-provider'import { Moon, Sun } from 'lucide-react'
export function ThemeToggle() { const { theme, setTheme } = useTheme()
return ( <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="fixed bottom-4 right-4 p-3 rounded-full bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors" aria-label="Toggle theme" > {theme === 'dark' ? ( <Sun className="h-5 w-5 text-yellow-500" /> ) : ( <Moon className="h-5 w-5 text-gray-700" /> )} </button> )}import { ThemeToggle } from '@/components/theme-toggle'
export default function Home() { return ( <div className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white"> <div className="container mx-auto px-4 py-8"> <h1 className="text-4xl font-bold mb-4"> Dark Mode Example </h1> <p className="text-lg mb-8"> このページはdarkモードに対応しています。 </p> <ThemeToggle /> </div> </div> )}3. Next.jsでの実装(Pages Router)
Section titled “3. Next.jsでの実装(Pages Router)”_app.tsxでの設定
Section titled “_app.tsxでの設定”import { ThemeProvider } from '@/contexts/theme-context'import type { AppProps } from 'next/app'import { useEffect, useState } from 'react'
export default function App({ Component, pageProps }: AppProps) { const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) { return null }
return ( <ThemeProvider> <Component {...pageProps} /> </ThemeProvider> )}ThemeContextの作成
Section titled “ThemeContextの作成”import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light'
type ThemeContextType = { theme: Theme toggleTheme: () => void}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState<Theme>('light') const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) const savedTheme = localStorage.getItem('theme') as Theme | null if (savedTheme) { setTheme(savedTheme) } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { setTheme('dark') } }, [])
useEffect(() => { if (mounted) { const root = document.documentElement root.classList.remove('light', 'dark') root.classList.add(theme) localStorage.setItem('theme', theme) } }, [theme, mounted])
const toggleTheme = () => { setTheme(prev => prev === 'dark' ? 'light' : 'dark') }
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> )}
export function useTheme() { const context = useContext(ThemeContext) if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider') } return context}4. React(Vite)での実装
Section titled “4. React(Vite)での実装”ThemeProviderの作成
Section titled “ThemeProviderの作成”import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
type Theme = 'dark' | 'light'
interface ThemeContextType { theme: Theme toggleTheme: () => void}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>(() => { if (typeof window !== 'undefined') { const saved = localStorage.getItem('theme') if (saved) { return saved as Theme } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } return 'light' })
useEffect(() => { const root = document.documentElement root.classList.remove('light', 'dark') root.classList.add(theme) localStorage.setItem('theme', theme) }, [theme])
const toggleTheme = () => { setTheme(prev => prev === 'dark' ? 'light' : 'dark') }
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> )}
export function useTheme() { const context = useContext(ThemeContext) if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider') } return context}main.tsxでの設定
Section titled “main.tsxでの設定”import React from 'react'import ReactDOM from 'react-dom/client'import App from './App.tsx'import { ThemeProvider } from './contexts/ThemeContext.tsx'import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <ThemeProvider> <App /> </ThemeProvider> </React.StrictMode>,)テーマ切り替えコンポーネント
Section titled “テーマ切り替えコンポーネント”import { useTheme } from '../contexts/ThemeContext'
export function ThemeToggle() { const { theme, toggleTheme } = useTheme()
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" > {theme === 'dark' ? ( <span className="text-yellow-500">☀️</span> ) : ( <span className="text-gray-700">🌙</span> )} </button> )}5. Tailwind CSSのdarkモードクラス
Section titled “5. Tailwind CSSのdarkモードクラス”基本的な使い方
Section titled “基本的な使い方”// dark:プレフィックスを使用<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white"> <h1 className="text-2xl font-bold">タイトル</h1> <p className="text-gray-600 dark:text-gray-400">説明文</p></div>よく使うパターン
Section titled “よく使うパターン”// 背景色className="bg-white dark:bg-gray-900"
// テキスト色className="text-gray-900 dark:text-white"
// ボーダーclassName="border-gray-200 dark:border-gray-700"
// ホバー効果className="hover:bg-gray-100 dark:hover:bg-gray-800"
// シャドウclassName="shadow-lg dark:shadow-gray-900/50"6. 実務での実装パターン
Section titled “6. 実務での実装パターン”パターン1: システム設定に従う
Section titled “パターン1: システム設定に従う”// システムの設定を監視useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => { setTheme(e.matches ? 'dark' : 'light') }
mediaQuery.addEventListener('change', handleChange) return () => mediaQuery.removeEventListener('change', handleChange)}, [])パターン2: カスタムフックの作成
Section titled “パターン2: カスタムフックの作成”import { useEffect, useState } from 'react'
export function useDarkMode() { const [theme, setTheme] = useState<'dark' | 'light'>('light') const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) const savedTheme = localStorage.getItem('theme') if (savedTheme) { setTheme(savedTheme as 'dark' | 'light') } }, [])
useEffect(() => { if (mounted) { document.documentElement.classList.toggle('dark', theme === 'dark') localStorage.setItem('theme', theme) } }, [theme, mounted])
return [theme, setTheme, mounted] as const}7. よくある問題と解決策
Section titled “7. よくある問題と解決策”問題1: フラッシュ(FOUC: Flash of Unstyled Content)
Section titled “問題1: フラッシュ(FOUC: Flash of Unstyled Content)”解決策: suppressHydrationWarningを使用
<html lang="ja" suppressHydrationWarning>問題2: サーバーサイドレンダリングでのエラー
Section titled “問題2: サーバーサイドレンダリングでのエラー”解決策: mounted状態をチェック
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true)}, [])
if (!mounted) { return null // またはデフォルトのUI}問題3: テーマが保存されない
Section titled “問題3: テーマが保存されない”解決策: localStorageの使用を確認
useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('theme', theme) }}, [theme])これで、Next.jsやReactアプリでTailwind CSSを使用したdarkモード実装が完了します。