Dark mode has become an essential feature in modern web applications. In this tutorial, we'll create a custom dark mode hook for Next.js using TypeScript, which supports system preferences and allows manual override.
Prerequisites
- Basic knowledge of React and Next.js
- Node.js installed on your machine
- A Next.js project set up with TypeScript
Step 1: Create the Dark Mode Hook
First, let's create our custom dark mode hook. Create a new file useDarkMode.ts
in your project's hooks
folder:
// hooks/useDarkMode.ts
import { useState, useEffect, useCallback } from 'react'
type Mode = 'light' | 'dark' | 'system'
export const useDarkMode = () => {
const [mode, setMode] = useState<Mode>('system')
const applyTheme = useCallback((isDark: boolean) => {
document.documentElement.classList.toggle('dark', isDark)
}, [])
const changeMode = useCallback((newMode: Mode) => {
setMode(newMode)
localStorage.setItem('theme', newMode)
}, [])
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Mode | null
if (savedTheme) {
setMode(savedTheme)
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
if (mode === 'system') {
applyTheme(event.matches)
}
}
const applyCurrentTheme = () => {
if (mode === 'system') {
applyTheme(mediaQuery.matches)
} else {
applyTheme(mode === 'dark')
}
}
applyCurrentTheme()
mediaQuery.addEventListener('change', handleSystemThemeChange)
return () => {
mediaQuery.removeEventListener('change', handleSystemThemeChange)
}
}, [mode, applyTheme])
return { mode, changeMode }
}
This hook manages the dark mode state, applies the theme to the document, and listens for system preference changes.
Step 2: Create a Dark Mode Provider
Now, let's create a provider component to wrap our app. Create a new file DarkModeProvider.tsx
in your project's components
folder:
// components/DarkModeProvider.tsx
import React, { createContext, useContext } from 'react'
import { useDarkMode } from '../hooks/useDarkMode'
type DarkModeContextType = ReturnType<typeof useDarkMode>
const DarkModeContext = createContext<DarkModeContextType | undefined>(undefined)
export const DarkModeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const darkMode = useDarkMode()
return (
<DarkModeContext.Provider value={darkMode}>
{children}
</DarkModeContext.Provider>
)
}
export const useDarkModeContext = () => {
const context = useContext(DarkModeContext)
if (context === undefined) {
throw new Error('useDarkModeContext must be used within a DarkModeProvider')
}
return context
}
This provider component will make our dark mode functionality available throughout the app.
Step 3: Wrap Your App with the Dark Mode Provider
Update your pages/_app.tsx
file to use the DarkModeProvider:
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { DarkModeProvider } from '../components/DarkModeProvider'
import '../styles/globals.css'
function MyApp({ Component, pageProps }: AppProps) {
return (
<DarkModeProvider>
<Component {...pageProps} />
</DarkModeProvider>
)
}
export default MyApp
Step 4: Create a Theme Toggle Component
Create a new component for the theme toggle button. Create a file ThemeToggle.tsx
in your components
folder:
// components/ThemeToggle.tsx
import React from 'react'
import { useDarkModeContext } from './DarkModeProvider'
const ThemeToggle: React.FC = () => {
const { mode, changeMode } = useDarkModeContext()
return (
<select
value={mode}
onChange={(e) => changeMode(e.target.value as 'light' | 'dark' | 'system')}
className="p-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-white"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}
export default ThemeToggle
Step 5: Add CSS for Light and Dark Themes
Update your styles/globals.css
file to include styles for both light and dark themes:
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
.dark {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
Step 6: Use the Theme in Your Components
Now you can use the theme in your components. Update your pages/index.tsx
:
// pages/index.tsx
import Head from 'next/head'
import ThemeToggle from '../components/ThemeToggle'
export default function Home() {
return (
<div className="min-h-screen p-4">
<Head>
<title>Dark Mode Demo</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-4">Welcome to Dark Mode Demo</h1>
<ThemeToggle />
<p className="mt-4">This is some sample text to show the theme change.</p>
</main>
</div>
)
}
Step 7: Configure Tailwind for Dark Mode
Update your tailwind.config.js
to enable the 'class' strategy for dark mode:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ... rest of your config
}
Step 8: Test Your Dark Mode
Run your Next.js application:
npm run dev
Visit http://localhost:3000
in your browser. You should now see your application with a working dark mode toggle that respects system preferences and allows manual override!
Conclusion
Congratulations! You've successfully implemented a custom dark mode hook in your Next.js application. This implementation uses React hooks for state management, respects system preferences, allows manual override, and uses Tailwind CSS for styling.
Remember to consider accessibility when implementing dark mode, ensuring that there's sufficient contrast between text and background colors in both themes.
Happy coding!
Top comments (0)