Dark Mode with Next.js and Tailwind CSS

Jia Yi

Jia Yi ยท 09 August 2021

5 min read

Next.js and Tailwind CSS Dark Mode

It is not uncommon to see dark mode on a web or mobile application, even Google has started integrating it into their desktop search. Some even consider both light and dark themes to be an integral part of the design.

In this article, you will learn how to add your very own dark mode using Next.js and Tailwind CSS.

Installing Next.js & Tailwind CSS

Start by creating a new base project with Next.js and Tailwind CSS. If you just want to add dark mode to your existing project, start by enabling dark mode.

npx create-next-app -e with-tailwindcss dark-mode
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p

Add the following 3 lines to globals.css

globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Enabling dark mode

Inside your tailwind.config.js, change darkMode to 'class' to enable toggling of themes.

tailwind.config.js
module.exports = {
  purge: [],
  darkMode: 'class', // change this to 'class'
  theme: {
    extend: {}
  },
  variants: {
    extend: {}
  },
  plugins: []
};

Next-themes

We will be using a 1.6kB sized package called next-themes to help us with the dark mode implementation.

Although it is possible to implement dark mode without the package. Here are a few reasons why next-themes is recommended:

  • Prevents flash of the wrong theme
  • Sync across multiple tabs
  • Stores preferred theme in localStorage

Install next-themes using npm and modify your _app.js by wrapping using the ThemeProvider provided by next-themes.

npm i next-themes
_app.js
import { ThemeProvider } from 'next-themes';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider attribute='class'>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;

Integrating next-themes

Next, we create a simple navigation bar with a toggle button to switch between the themes.

Navbar.js
const Navbar = () => {
  return (
    <div className='items-center'>
      <div className='bg-gray-200 dark:bg-gray-600 rounded-b-md'>
        <div className='flex flex-col flex-wrap p-1 mx-auto items-center md:flex-row'>
          <nav className='flex flex-wrap items-center justify-start text-base '>
            <ul className='items-center inline-block list-none md:inline-flex'>
              <li>
                <a
                  href='#'
                  className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
                >
                  Link 1
                </a>
              </li>
              <li>
                <a
                  href='#'
                  className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
                >
                  Link 2
                </a>
              </li>
            </ul>
          </nav>
          <button className='w-auto mx-4 px-2 py-2 my-2 text-base font-medium text-white bg-gray-400 rounded-md md:ml-auto'>
            Toggle
          </button>
        </div>
      </div>
    </div>
  );
};

export default Navbar;

There is just one thing we need to note, right from the documentation:

...and will throw a hydration mismatch warning when rendering with SSG or SSR.

We need to ensure the component is mounted using useEffect hook before trying to render the toggle button to avoid hydration mismatch. A toggleTheme function will be passed to the button's onClick event handler to toggle between the themes.

We will also bring in a couple of icons from Heroicons to represent our dark and light themes.

Navbar.js
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

const Navbar = () => {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  const toggleTheme = () => {
    setTheme(theme === 'dark' ? 'light' : 'dark');
  };

  return (
    <div className='items-center'>
      <div className='bg-gray-200 dark:bg-gray-600 rounded-b-md'>
        <div className='flex flex-col flex-wrap p-1 mx-auto items-center md:flex-row'>
          <nav className='flex flex-wrap items-center justify-start text-base '>
            <ul className='items-center inline-block list-none md:inline-flex'>
              <li>
                <a
                  href='#'
                  className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
                >
                  Link 1
                </a>
              </li>
              <li>
                <a
                  href='#'
                  className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
                >
                  Link 2
                </a>
              </li>
            </ul>
          </nav>
          {mounted && (
            <button
              onClick={toggleTheme}
              className='w-auto mx-4 px-2 py-2 my-2 text-base font-medium text-white bg-gray-400 rounded-md md:ml-auto'
            >
              {theme === 'dark' ? (
                <svg
                  xmlns='http://www.w3.org/2000/svg'
                  className='h-5 w-5'
                  viewBox='0 0 20 20'
                  fill='currentColor'
                >
                  <path
                    fillRule='evenodd'
                    d='M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z'
                    clipRule='evenodd'
                  />
                </svg>
              ) : (
                <svg
                  xmlns='http://www.w3.org/2000/svg'
                  className='h-5 w-5'
                  viewBox='0 0 20 20'
                  fill='currentColor'
                >
                  <path d='M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z' />
                </svg>
              )}
            </button>
          )}
        </div>
      </div>
    </div>
  );
};

export default Navbar;
Note that...
You can add the mounting delay in the component of your choice.

Implemented dark mode

You'll have your very own dark mode implemented with next.js and Tailwind CSS. No FOUC, no jankiness. Perfect! ๐Ÿ‘Œ

Resources