DEV Community

Cover image for Implementing a Custom Dark Mode Hook in Next.js
Sanon Joas
Sanon Joas

Posted on

Implementing a Custom Dark Mode Hook in Next.js

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 }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Test Your Dark Mode

Run your Next.js application:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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)