First, why do we need to implement dark and light themes in our projects?

Having this feature is not a requirement that will necessarily bring more traffic to your project, but it greatly improves the user experience, as people have different preferences.
With that understood, let's dive into how to do it! 😄

Understanding the feature in Tailwind

To configure this feature with Tailwind, you just need to add or remove the dark class on the element and use the dark: prefix in the elements’ classes that you want to style differently based on the theme.

Example:

In this example, the default background color will be bg-gray-800 (light mode), and when the dark theme is active, it will switch to bg-gray-500.

How it works in Next.js

In Next.js, it’s a bit different, because you’ll likely need some help from a library called next-themes.
Important: it’s not mandatory — you can achieve the same functionality manually — but using the library makes the development process much easier.

Let’s see examples both without and with the library.

Example without using the library

In this case, you are responsible for:

  • Managing the theme value in localStorage;
  • Detecting the user’s system theme to provide a better experience.
// hooks/useTheme.ts
import { useEffect, useState } from 'react';

type Theme = 'light' | 'dark';

export function useTheme() {
  const [theme, setTheme] = useState('light');

  // Detect system preferences
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme') as Theme | null;
    if (savedTheme) {
      setTheme(savedTheme);
    } else {
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      setTheme(prefersDark ? 'dark' : 'light');
    }
  }, []);

  // Update  class and save to localStorage
  useEffect(() => {
    const root = document.documentElement;
    if (theme === 'dark') {
      root.classList.add('dark');
      root.classList.remove('light');
    } else {
      root.classList.add('light');
      root.classList.remove('dark');
    }
    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}

Example using the next-themes library

// provider/theme-provider.ts
'use client'

import { ThemeProvider as NextThemesProvider } from 'next-themes'
import * as React from 'react'

function ThemeProvider({ children, ...props }: React.ComponentProps) {
  const [mounted, setMounted] = React.useState(false)

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

  if (!mounted) {
    return <>{children}>
  }

  return {children}
}

export { ThemeProvider }

And using it in your root layout:

async function RootLayout({ children }: { children: ReactNode }) {
  return (
    
      
        
          {children}
        
      
    
  )
}

When using the next-themes library, you get many built-in features like:

  • defaultTheme="system", which automatically picks up the user’s system preference;
  • Managing theme changes through the classattribute;
  • Other customizations available in the official documentation: 👉 next-themes on npm

Conclusion

Having dark and light themes in your system is not absolutely critical, but it is very important to improve usability and the overall user interaction with your project.