Implementing dark mode with NextJS and TailwindCSS

In today’s web, providing users with a pleasant browsing experience is essential. One popular way to achieve this is by adding a light/dark mode toggle to your website. In this blog post, I’ll walk you through implementing this functionality using NextJS and TailwindCSS. We’ll create a dropdown menu that allows users to choose between light, dark, and system modes.

Desired outcome

There are several ways to implement dark mode, but one of the best examples I’ve seen is how it’s done on the TailwindCSS docs. Users can choose between light, dark, and system mode, meaning they either manually select dark or light and have their preference saved to localStorage, or they choose to respect the OS setting.

I think this approach is the best of both worlds, as users have the flexibility to select a specific mode for each website or tool they use. The desired functionality involves a dropdown with three settings and corresponding logos. When users manually select dark or light mode, a colored logo will indicate the chosen setting. By default, the system mode should be selected.

Tools for implementation

To implement this functionality, we will use the following tools:

Steps for implementation

  1. First make sure all dependencies are installed:
 pnpm install tailwindcss next next-themes
  1. Wrap your application with the ThemeProvider component provided bynext-themes in your _app.tsx file:
import { ThemeProvider } from 'next-themes'

function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

export default App
  1. The useTheme hook provides the current theme (theme) and a function to toggle the theme (setTheme). You can use these values to customize the appearance of your components based on the selected theme.
import { useTheme } from 'next-themes'

function Component() {
  const { theme, setTheme } = useTheme()

  function toggleTheme() {
    setTheme(theme === 'dark' ? 'light' : 'dark')
  }

  return (
    <div>
      <button onClick={toggleTheme}>Toggle theme</button>
      <p>Current theme: {theme}</p>
    </div>
  )
}

export default Component

Example for implementation

This is a working example that recreates the functionality of the TailwindCSS docs. This example uses Radix UI for the dropdown menu.

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as ToggleGroup from '@radix-ui/react-toggle-group'
import { IconDeviceLaptop, IconMoon, IconSun } from '@tabler/icons-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'

function ThemeDropdown() {
  const [mounted, setMounted] = useState(false)
  const { theme, resolvedTheme, setTheme } = useTheme()

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

  if (!mounted) {
    return null
  }
  return (
    <>
      <DropdownMenu.Root>
        <DropdownMenu.Trigger>
          {resolvedTheme === 'light' && theme === 'light' ? (
            <IconSun size={30} className='text-blue-400' />
          ) : resolvedTheme === 'light' ? (
            <IconSun size={30} />
          ) : resolvedTheme === 'dark' && theme === 'dark' ? (
            <IconMoon size={30} className='text-blue-300' />
          ) : resolvedTheme === 'dark' ? (
            <IconMoon size={30} />
          ) : (
            <IconDeviceLaptop size={30} />
          )}
        </DropdownMenu.Trigger>

        <DropdownMenu.Portal>
          <DropdownMenu.Content>
            <DropdownMenu.Label />
            <DropdownMenu.Item />

            <ToggleGroup.Root
              type='single'
              orientation='vertical'
              value={theme}
              aria-label='Dark/Light/System Mode Selection'
              className='flex flex-col items-start w-full gap-1 pt-1 m-2 border-2 rounded-md bg-base-light dark:bg-base-dark border-base-dark dark:border-base-light'
            >
              <ToggleGroup.Item
                value='light'
                onClick={() => setTheme('light')}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark dark:hover:text-base-dark ${
                  theme === 'light' ? 'font-bold' : 'font-normal'
                }`}
              >
                <IconSun size={30} />
                <span>Light</span>
              </ToggleGroup.Item>

              <ToggleGroup.Item
                value='dark'
                onClick={() => setTheme('dark')}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark dark:hover:text-base-dark ${
                  theme === 'dark' ? 'font-bold' : 'font-normal'
                }`}
              >
                <IconMoon size={30} />
                <span>Dark</span>
              </ToggleGroup.Item>

              <ToggleGroup.Item
                value='system'
                onClick={() => setTheme('system')}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark hover:text-base-dark ${
                  theme === 'system' ? 'font-bold' : 'font-normal'
                }`}
              >
                <IconDeviceLaptop size={30} />
                <span>System</span>
              </ToggleGroup.Item>
            </ToggleGroup.Root>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>
    </>
  )
}

export default ThemeDropdown

The resolvedTheme variable is used to check the currently selected theme(light/dark/system). By default, the system mode is selected, and the logo associated with it is displayed. When users manually choose between dark or light mode, the corresponding logo will indicate the selected setting.

Conclusion

In this tutorial, we have explored how to implement a light/dark mode toggle using NextJS and TailwindCSS. By giving users the flexibility to choose between light, dark, and system modes, we can enhance the user experience by catering to their individual preferences.