Cómo agregar Dark Mode con TailwindCSS en Next.js

Stewart Granger Flores - 08 Noviembre, 2020

Next.js Dark Mode

Voy a asumir que ya tienes tu proyecto Next.js configurado con Tailwind, sino, puedes ver mi tutorial de cómo instalar Tailwind en tu proyecto

¿Por qué Next Themes?

Next Themes nos ofrece Hooks para poder manejar el localStorage del usuario sin tener que preparar la configuración nosotros, esto nos permite evitar los típicos problemas de hidratación (cuando hay un flash blanco antes de cargar el tema oscuro en tu página, o el icono de tu Dark Mode no se carga correctamente, etc, etc..)

1-¿Qué instalar?

Ahora instalaremos lo esencial para que esto funcione: Next Themes

 
       npm install next-themes
       

2-Configuraciones

Ahora en nuestro Tailwind.config.js asegurense de cambiar darkMode a '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- Preparaciones

Iremos a _app.js y agregaremos el Provider que nos entrega 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;
  

Ahora abrimos el componente donde quieres dejar tu Toggle para cambiar de tema, el Navbar por ejemplo e importamos useTheme que es el Hook que nos permite modificar el localStorage y obtenerlo.

  
    import { useTheme } from "next-themes"
    

De aquí destructuraremos theme y setTheme

  
    const { theme, setTheme } = useTheme();
    

Ahora, como cambiaremos el tema en el lado del cliente, usaremos un State para saber si la página ya fue montada o no.

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

Ahora asegurense de que este es el primer useEffect que se ejecuta en la vista.

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

Y creamos la función que nos deja cambiar el tema:

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

Ahora tu código debería verse algo así

  
      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>
        );
      }
    

Con esto ya deberías poder modificar tu tema oscuro, pero antes...

4- Evitando problemas de hidratación

Ahora tenemos control sobre los estados y el dark mode de la página, pero aún falta un paso importante.

Debido a que no podemos qué tema tiene escogido como predeterminado el usuario por el lado del servidor, useTheme será undefined hasta que se monte el lado del cliente. Esto significa que si cargas componentes basados en el tema del usuario (el típico Sol o Ampolleta para los que están en Dark Mode y Lunas para los que están en Light Mode) habrá un periodo donde no concuerden con el tema correcto.


Esto es un factor tomado en cuenta por Google Lighthouse para evitar disconformidad para el usuario. Mejor conocido como Cumulative Layout Shift (CLS) y gira en torno a la estabilidad de la página en su interfaz de usuario.

Para poder arreglar esto, haremos lo siguiente

  
      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>
        );
      }
    

Agregaremos if (!isMounted) return null antes de renderizar la página para esperar a que el componente esté montado y sepamos qué tema renderizar al usuario.

Con esto deberías poder agregar clases según quieras a tu página de la siguiente forma:

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


Acá pueden ver un ejemplo de cómo se ve el Dark Mode funcionando