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.
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
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
}
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 switchingdefaultTheme="system"
: Defaults to the system preferenceenableSystem
: Enables detection of system preferenceNextReady 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>
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.
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.
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>
)
}
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'.
For images that need different versions in light and dark modes, you can use CSS or conditional rendering:
/* 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" />
'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}
/>
)
}
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>
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 */
}
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>
)
}
/* Add to your CSS for smooth transitions */
* {
transition: background-color 0.3s ease, color 0.3s ease;
}
If the theme is not applying correctly, check the following:
If you're experiencing a flash of incorrect theme on page load:
// 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) {}
})()
` }} />
After implementing dark mode, consider these next steps: