Integrating TailwindCSS dark mode with NextJS

·

15 min read

Integrating TailwindCSS dark mode with NextJS

The essentials

On the modern web, dark mode has become pretty much the standard. However, there're users who prefer light mode in certain situations. In any case, the more options you give your users to customize their experience, the better.

Note

It is assumed you have a NextJS project set up and TailwindCSS installed within it. If you don't, you can easily run npx create-next-app@latest to create an empty project, where you will be prompted to add TailwindCSS as the recommended way to write your styling.

Activating dark mode

Now, we have to edit the tailwind.config.js file in order to configure the library. It's as easy as adding one line

tailwind.config.js
module.exports = {
  content: [
    ...
  ],
  mode: "jit",
  darkMode: "class",
  theme: {
    ...
  },
  plugins: [],
};

By adding the darkMode: "class" directive, you're telling tailwind to add the utility classes to support dark mode. Now you can add the dark prefix to your classes and the correct class will be served to your page.

For example, to change the background or text color depending on the page theme, you can use a styling like this

src/app/page.tsx
<div className="bg-gray-100 dark:bg-gray-800">
  <span className="text-black dark:text-white">
    Your content
  </span>
</div>

The default classes will be applied as a light theme, and the ones prefixed with dark will override them when the color-scheme: dark style is present in the document.

Toggle between themes

You now have a working application that uses tailwind classes to style your components according to the current theme. But how do you toggle between themes? To implement this, we will use the next-themes library. To add it to your project, run the following command

npm install next-themes

Now, as of date of writing this article, NextJS 13 has implemented the app router as the recommended way to build applications. By default, all new components will be React Server Components, in which it isn't possible to use hooks. Because of this, we will have to make sure to add the "use client"; directive on top of all the files that import hooks.

Adding a Theme Provider

Initially, we have to add the library's ThemeProvider component in order to use and change the current theme of the application.

In the app directory, create a Providers.tsx component

app/Providers.tsx
"use client";

import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="dark">
      {children}
    </ThemeProvider>
  );
}

In this file, we export a component that takes a React Node as props and wraps the next-themes ThemeProvider around it. This is the recommended way to add providers in NextJS 13, due to the necessity of the "use client"; directive on top of the file.

Now, add this component inside the body of your layout

src/app/layout.tsx
import { Providers } from "./Providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Note that it's necessary to add suppressHydrationWarning to your html component, because it will be updated on the client. This only applies one level deep into the DOM, so don't worry about not catching other errors in your application.

Adding a Toggle Theme button

Now let's create a button with which the user can change the current theme.

src/app/components/ThemeToggle.tsx
"use client";
import React, { useState, useEffect } from "react";
import { useTheme } from "next-themes";

export default function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }
  const handleToggleTheme = () => {
    setTheme(theme === "dark" ? "light" : "dark");
  };

  return (
    <button
      onClick={handleToggleTheme}
    >
      {theme === "dark" ? (
        <div>Dark mode!</div>
          ) : (
        <div>Light mode!</div>
      )}
    </button>
  );
}

Let's break all of this code down.

First of all, we import the useTheme hook from next-themes, so, again, we need to add the "use client"; directive. Now, we can destructure this hook in order to get the current theme and a setTheme function.

Using these two properties we can now write a handleToggleTheme function and set the theme accordingly

src/app/components/ThemeToggle.tsx
const handleToggleTheme = () => {
  setTheme(theme === "dark" ? "light" : "dark");
};

The library also gives the possibility for the theme to be set to "system", so feel free to add this option if it suits your application.

The selected theme will be saved to localStorage when it is changed, so we're also adding persistance to this state.

Let's set this function to the onClick property of our button, and we're pretty much done! There's just one more this we need to do for the component to work property.

Even though we indicated NextJS that this component is a Client Component, it's still being pre-rendered on the server, so using it as it is will throw a hydration error. To fix this we can to add a useEffect hook, since it only runs on the client

src/app/components/ThemeToggle.tsx
import React, { useState, useEffect } from "react";

const [mounted, setMounted] = useState(false);

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

if (!mounted) {
  return null;
}

The useEffect hook will only run on the initial render, setting the mounted state to true. Now the button will be rendered on the client, avoiding any hydration errors.

Now you can use this button anywhere in your app.