Dark Mode with Tailwind CSS and Next.js

Published on 24/01/2021

Last updated on 28/03/2024


This quick little piece is to help you implement, in a very streamlined fashion, a dark mode toggle in your website using Tailwind CSS and Next.js.

Before getting it down, I was battling my brain out at trying to set this up, especially given Next.js's server-side rendering stuff. I was having trouble using the usual client-side local.storage method for saving the preferred theme on the browser's cache. I naturally got a little overwhelmed as it seemed like a much harder task than I could've ever hoped for when I initially started venturing into this.

But after researching all articles around and asking for help late at night from my dev friends, I got to a very, very simple setup using these two frameworks.

Setting up the stack

next-themes

It all goes down to this library. And it does what it says: "perfect dark mode in 2 lines of code". Easy peasy. Just install it on your project using any of these two:

1npm install next-themes

Once it is installed, go to the _app.js file on your project, which is automatically created after installing Next, and just wrap all of the elements of the MyApp component with the ThemeProvider, just like so:

pages/app.js
1import { ThemeProvider } from 'next-themes'
2
3export default function MyApp({ Component, pageProps }) {
4  return (
5    <ThemeProvider>
6      <Component {...pageProps} />
7    </ThemeProvider>
8  );
9}

Tailwind CSS

First, make sure to properly install Tailwind by following their guide tailored focused on a Next.js project. Once that's done, go to your tailwind.config.js file and change the dark mode property to class, just like that:

/tailwind.config.js
1module.exports = {
2  darkMode: 'class'
3}

Now, go back to the _app.js file and just add "class" as the attribute value on the Theme Provider component:

pages/_app.js
1<ThemeProvider attribute="class">{...}</ThemeProvider>

Creating the theme toggle button

With everything set up, we now need to create the button that will be responsible for toggling dark mode on and off. I promise you it's not super complex! Let's start by creating a DarkModeButton.js file in your components folder (create one if you don't have one yet).

components/DarkModeButton.js
1import React from 'react'
2
3export default function DarkModeButton() {
4    return (
5        <button></button>
6    );
7}

We'll use hooks to add functionality. These will be basically responsible for toggling the dark class, mounting the component and theme as a whole, and also switching the icons that we'll represent each theme within the button:

components/DarkModeButton.js
1import React, { useEffect, useState } from 'react';
2import { useTheme } from 'next-themes';
3
4export default function DarkModeButton() {
5  const { theme, setTheme } = useTheme();
6  const [mounted, setMounted] = useState(false);
7  useEffect(() => setMounted(true), []);
8
9    return (
10        <button></button>
11    );
12}

Now comes the fun part of actually designing the button! Don't forget to add the dark: prefix class to other classes that should be applied to the dark mode only. Feel free to start with the styles I've added to my button.

Note that I went ahead and already added the aria-label attribute, which is important for accessibility (enabling screen readers to properly read what the button is about).

components/DarkModeButton.js
1import React, { useEffect, useState } from 'react';
2import { useTheme } from 'next-themes';
3
4export default function DarkModeButton() {
5  const { theme, setTheme } = useTheme();
6  const [mounted, setMounted] = useState(false);
7  useEffect(() => setMounted(true), []);
8
9  return (
10    <button
11      aria-label="Appearance mode toggle button"
12      type="button"
13      className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800  dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7">
14    </button>
15  );
16}

Getting closer to the end, add the onClick event to your button element and pass the setTheme hook that will enable us to toggle between themes:

components/DarkModeButton.js
1<button
2  aria-label="Appearance mode toggle button"
3  type="button"
4  className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800  dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7"
5  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
6</button>

Lastly, we'll now insert a sun and moon icon inside the button so the user knows which theme they'll get by clicking on it. Use whatever icons you want for this-I'm personally using Hero Icons.

components/DarkModeButton.js
1import React, { useEffect, useState } from 'react';
2import { useTheme } from 'next-themes';
3import { SunIcon, MoonIcon } from "@heroicons/react/20/solid";
4
5const styles = "h-4 w-4 text-gray-600 dark:text-gray-300";
6
7export default function DarkModeButton() {
8  const { theme, setTheme } = useTheme();
9  const [mounted, setMounted] = useState(false);
10  useEffect(() => setMounted(true), []);
11
12  return (
13    {mounted && (
14      <button
15        type="button"
16        className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800  dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7"
17        aria-label={
18          theme === "dark" ? "Toggle light mode" : "Toggle dark mode"
19        }
20        onClick={() => {
21          setTheme(theme === "dark" ? "light" : "dark");
22        }}
23      >
24        {theme === "dark" ? (
25          <SunIcon className={styles} />
26        ) : (
27          <MoonIcon className={styles} />
28        )}
29      </button>
30    )}
31  );
32}

Extra controls

On the Theme Provider component, use the defaultTheme prop to determine which theme is rendered by default, and the enableSystem prop to control whether you want your application respect the user's system appearance mode.

pages/_app.js
1<ThemeProvider
2  attribute="class"
3  defaultTheme="dark"
4  enableSystem={false}
5>
6  {...}
7</ThemeProvider>

Wrap up

Woof, that's it! ⚡️🎉 You should now have a functioning dark mode toggle button on your website/app. This is a particularly cool implementation as it avoids a common pitfall with dark mode out there: the annoying flashy-ness. Tailwind also makes tailoring your design for dark mode super easy. It's tough to have neat 1:1 light and dark mode color scale mapping, particularly as you will frequently need special treatment to preserve the interface's depth & hierarchy.

I hope this wasn't too hard to follow, and that it helped you solve something that took me literally freaking weeks to figure out. Also, definitely visit the next-themes documentation; most of your further questions should be answered there! Enjoy! 🤙