NextReady

Dark Mode Implementation

NextReady comes with a fully implemented dark mode system that seamlessly integrates with Next.js and Tailwind CSS. This guide explains how the dark mode is implemented and how you can customize it for your application.

Key Features

  • System preference detection
  • Manual toggle between light and dark modes
  • Persistent user preference across sessions
  • Smooth transitions between themes
  • Tailwind CSS integration
  • Support for custom color schemes

Implementation Details

Dependencies

The dark mode implementation uses the next-themes library, which provides an easy way to add dark mode support to Next.js applications:

npm install next-themes

Theme Provider

The core of the dark mode implementation is the ThemeProvider component, which wraps your application and provides theme context:

// src/components/providers/theme-provider.tsx
'use client'

import { createContext, useContext, useEffect, useState } from 'react'
import { useTheme } from 'next-themes'

const ThemeProviderContext = createContext({
  theme: 'system' as string,
  setTheme: (theme: string) => {},
})

export function ThemeProvider({
  children,
  ...props
}: {
  children: React.ReactNode
}) {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()

  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return null
  }

  return (
    <ThemeProviderContext.Provider value={{ theme: theme || 'system', setTheme }}>
      {children}
    </ThemeProviderContext.Provider>
  )
}

export const useThemeContext = () => {
  const context = useContext(ThemeProviderContext)
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

Application Provider Setup

The ThemeProvider is integrated into the application through the Providers component:

// src/app/providers.tsx
'use client'

import * as React from 'react'
import { ThemeProvider } from 'next-themes'
import { SessionProvider } from 'next-auth/react'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
      >
        {children}
      </ThemeProvider>
    </SessionProvider>
  )
}

The key configuration options for the ThemeProvider are:

  • attribute="class": Uses CSS classes for theme switching
  • defaultTheme="system": Defaults to the system preference
  • enableSystem: Enables detection of system preference

Tailwind CSS Configuration

NextReady uses Tailwind CSS for styling, which has built-in support for dark mode. The configuration in tailwind.config.js enables dark mode using the class strategy:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // other configuration...
  theme: {
    extend: {
      colors: {
        // Custom colors that support dark mode
        background: {
          DEFAULT: 'var(--background)',
          secondary: 'var(--background-secondary)',
        },
        text: {
          primary: 'var(--text-primary)',
          secondary: 'var(--text-secondary)',
        },
        // other colors...
      },
    },
  },
}

With this configuration, you can use Tailwind's dark mode variant to apply different styles in dark mode:

<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
  This element will have a white background and black text in light mode,
  and a dark gray background with white text in dark mode.
</div>

Theme Toggle Component

NextReady includes a theme toggle component that allows users to switch between light and dark modes:

// src/components/theme-toggle.tsx
'use client'

import { useTheme } from 'next-themes'
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline'

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="rounded-full p-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-primary-rose dark:focus:ring-offset-gray-900"
      aria-label="Toggle dark mode"
    >
      {theme === 'dark' ? (
        <SunIcon className="h-5 w-5 text-yellow-300" />
      ) : (
        <MoonIcon className="h-5 w-5 text-gray-700" />
      )}
    </button>
  )
}

This component can be placed in your layout or navigation to provide users with an easy way to toggle between themes.

CSS Variables for Theme Colors

To provide a consistent color scheme across both light and dark modes, NextReady uses CSS variables defined in the global CSS file:

/* globals.css */
:root {
  /* Light mode variables */
  --background: #ffffff;
  --background-secondary: #f9fafb;
  --text-primary: #111827;
  --text-secondary: #6b7280;
  /* other variables... */
}

.dark {
  /* Dark mode variables */
  --background: #111827;
  --background-secondary: #1f2937;
  --text-primary: #f9fafb;
  --text-secondary: #d1d5db;
  /* other variables... */
}

/* Use these variables in your Tailwind config */

These CSS variables are then referenced in the Tailwind configuration, allowing you to use semantic color names that automatically adapt to the current theme.

Using Dark Mode in Components

Accessing Theme in React Components

You can access the current theme and theme-switching function in any React component:

'use client'

import { useTheme } from 'next-themes'

export function MyComponent() {
  const { theme, setTheme } = useTheme()
  
  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme('light')}>Light Mode</button>
      <button onClick={() => setTheme('dark')}>Dark Mode</button>
      <button onClick={() => setTheme('system')}>System Preference</button>
    </div>
  )
}

Conditional Rendering Based on Theme

You can conditionally render different content based on the current theme:

'use client'

import { useTheme } from 'next-themes'

export function ThemeAwareComponent() {
  const { resolvedTheme } = useTheme()
  
  return (
    <div>
      {resolvedTheme === 'dark' ? (
        <p>Dark mode content</p>
      ) : (
        <p>Light mode content</p>
      )}
    </div>
  )
}

Note: Use resolvedTheme instead of theme when you need to know the actual theme being applied. This is especially important when the theme is set to 'system', as resolvedTheme will tell you whether the system preference resulted in 'light' or 'dark'.

Handling Images in Dark Mode

For images that need different versions in light and dark modes, you can use CSS or conditional rendering:

CSS Approach

/* In your CSS */
.theme-image {
  content: url('/path/to/light-image.png');
}

.dark .theme-image {
  content: url('/path/to/dark-image.png');
}

/* In your JSX */
<img className="theme-image" alt="Theme-aware image" />

React Approach

'use client'

import Image from 'next/image'
import { useTheme } from 'next-themes'

export function ThemeAwareImage() {
  const { resolvedTheme } = useTheme()
  
  return (
    <Image
      src={resolvedTheme === 'dark' 
        ? '/path/to/dark-image.png' 
        : '/path/to/light-image.png'
      }
      alt="Theme-aware image"
      width={200}
      height={200}
    />
  )
}

Customizing the Dark Mode

Changing Default Theme

To change the default theme, update the defaultTheme prop in the ThemeProvider:

<ThemeProvider
  attribute="class"
  defaultTheme="dark" // Changed from "system" to "dark"
  enableSystem
>
  {children}
</ThemeProvider>

Adding Custom Color Themes

You can extend the theme system to support more than just light and dark modes:

// 1. Update your ThemeProvider
<ThemeProvider
  attribute="class"
  defaultTheme="system"
  enableSystem
  themes={['light', 'dark', 'purple', 'green']} // Add custom themes
>
  {children}
</ThemeProvider>

// 2. Add CSS classes for each theme
/* globals.css */
:root {
  /* Light theme variables */
}

.dark {
  /* Dark theme variables */
}

.purple {
  --background: #f3e8ff;
  --text-primary: #581c87;
  /* other purple theme variables */
}

.green {
  --background: #ecfdf5;
  --text-primary: #064e3b;
  /* other green theme variables */
}

Theme Selector Component

For multiple themes, you can create a theme selector component:

'use client'

import { useTheme } from 'next-themes'

export function ThemeSelector() {
  const { theme, setTheme } = useTheme()
  
  return (
    <select
      value={theme}
      onChange={(e) => setTheme(e.target.value)}
      className="p-2 rounded border border-gray-300 dark:border-gray-700"
    >
      <option value="system">System Preference</option>
      <option value="light">Light Mode</option>
      <option value="dark">Dark Mode</option>
      <option value="purple">Purple Theme</option>
      <option value="green">Green Theme</option>
    </select>
  )
}

Best Practices

  • Handle Flash of Incorrect Theme: The mounted state check in the ThemeProvider prevents the flash of incorrect theme on initial load.
  • Use Semantic Color Names: Instead of using specific color names like "gray-900", use semantic names like "text-primary" that can adapt to the current theme.
  • Test Both Themes: Always test your application in both light and dark modes to ensure good contrast and readability.
  • Respect User Preference: Default to the system preference but allow users to override it.
  • Smooth Transitions: Add CSS transitions for color changes to make theme switching feel smooth.
/* Add to your CSS for smooth transitions */
* {
  transition: background-color 0.3s ease, color 0.3s ease;
}

Troubleshooting

Theme Not Applying Correctly

If the theme is not applying correctly, check the following:

  • Ensure the ThemeProvider is wrapping your application
  • Verify that Tailwind's darkMode is set to 'class' in the configuration
  • Check for any CSS that might be overriding your theme styles
  • Make sure you're using the dark: variant correctly in your Tailwind classes

Flash of Incorrect Theme

If you're experiencing a flash of incorrect theme on page load:

  • Ensure you're checking the mounted state before rendering theme-dependent content
  • Consider adding a script to the head of your HTML to set the theme class before React hydration
// Add this script to your document head
<script dangerouslySetInnerHTML={{ __html: `
  (function() {
    try {
      const theme = localStorage.getItem('theme') || 'system';
      
      if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark');
      } else {
        document.documentElement.classList.remove('dark');
      }
    } catch (e) {}
  })()
` }} />