How to add Dark Mode with TailwindCSS in Next.js

Stewart Granger Flores - 08 Noviembre, 2020

Next.js Dark Mode

I'm going to assume that you already have your Next.js project configured with Tailwind, if not, you can see my tutorial of how to install Tailwind in your project

Why Next Themes?

Next Themes offers Hooks to manage the user's localStorage without having to prepare the configuration ourselves, this allows us to avoid the typical hydration problems (when there is a white flash before loading the dark theme on your page, or the icon of your Dark Mode does not load correctly, etc, etc...).

1-What to install?

Now we will install the essentials for this to work: Next Themes

 
       npm install next-themes
       

2-Configurations

Now in our Tailwind.config.js make sure to change darkMode to 'class'

 
  const colors = require("tailwindcss/colors");

  module.exports = {
    purge: [],
    presets: [],
    darkMode: "class", // or 'media' or 'class'
    theme: {
      screens: {
        sm: "640px",
        md: "768px",
        lg: "1024px",
        xl: "1280px",
        "2xl": "1536px",
      },
      colors: {
        transparent: "transparent",
        current: "currentColor"
       

3- Preparations

We will go to _app.js and add the Provider provided by next-themes

  
  import "../styles/globals.css";
  import { ThemeProvider } from "next-themes";

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

  export default MyApp;
  

Now we open the component where you want to leave your Toggle to change the theme, the Navbar for example and we import useTheme which is the Hook that allows us to modify the localStorage and get it.

  
    import { useTheme } from "next-themes"
    

From here we will destruct theme and setTheme

  
    const { theme, setTheme } = useTheme();
    

Now, as we will change the theme on the client side, we will use a State to know if the page has already been mounted or not.

  
    const [isMounted, setIsMounted] = useState(false);
    

Now make sure that this is the first useEffect that is executed in the view.

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

And we create the function that lets us change the theme:

  
    const switchTheme = () => {
        if (isMounted) {
          setTheme(theme === "light" ? "dark" : "light");
        }
    };
    

Now your code should look something like this

  
      import { useEffect, useState } from "react";
      import { useTheme } from "next-themes";
      export default function Navbar() {
        const [isMounted, setIsMounted] = useState(false);
        const { theme, setTheme } = useTheme();
      useEffect(() => {
          setIsMounted(true);
        }, []);
      const switchTheme = () => {
          if (isMounted) {
            setTheme(theme === "light" ? "dark" : "light");
          }
        };
      return (
          <div className="text-center">
            <button onClick={switchTheme}>Cambiar tema</button>
          </div>
        );
      }
    

With this you should now be able to modify your dark theme, but first...

4- Avoiding hydration problems

We now have control over the states and dark mode of the page, but there is still an important step missing.

Since we can't tell which theme the user has chosen as default on the server side, useTheme will be undefined until the client side is assembled. This means that if you load components based on the user's theme (the typical Sun or Light Bulb for those in Dark Mode and Moons for those in Light Mode) there will be a period where they do not match the correct theme.


This is a factor taken into account by Google Lighthouse to avoid user dissatisfaction. Better known as Cumulative Layout Shift (CLS) and revolves around the stability of the page in its user interface.

In order to fix this, we will do the following

  
      import { useEffect, useState } from "react";
      import { useTheme } from "next-themes";
      export default function Navbar() {
        const [isMounted, setIsMounted] = useState(false);
        const { theme, setTheme } = useTheme();
      useEffect(() => {
          setIsMounted(true);
        }, []);
      const switchTheme = () => {
          if (isMounted) {
            setTheme(theme === "light" ? "dark" : "light");
          }
        };
      if (!isMounted) return null // <-- Agregar esto
      return (
          <div className="text-center">
            <button onClick={switchTheme}>Cambiar tema</button>
          </div>
        );
      }
    

We will add if (!isMounted) return null before rendering the page to wait until the component is assembled and we know which theme to render to the user.

With this you should be able to add classes as you wish to your page as follows:

  
    <h1 className="text-black dark:text-white"> Título de la página </h1>
    


Here you can see an example of how Dark Mode looks like working