DEV Community

Eric Luna
Eric Luna

Posted on

Dark mode con TailwindCSS y NextJS

Cada día me sorprendo más de la velocidad para escribir CSS que me da TailwindCSS.

Hace un par de semanas implementé el tema oscuro de este sitio y fue una experiencia sin ningún tipo de fricción.

⚙️ Configuración inicial

En la raíz de nuestro proyecto vamos a tener un archivo tailwind.config.js después de haber instalado Tailwiind:

tailwind.config.js

module.exports = {
    ...
  darkMode: 'class'
    ...
}
Enter fullscreen mode Exit fullscreen mode

Lo que hace esto es habilitar el modo oscuro cuando agregamos la clase dark a la etiqueta html de nuestro proyecto.

<html className="dark">
    ....
</html>
Enter fullscreen mode Exit fullscreen mode

Una vez habilitado el tema oscuro vamos a poder agregar el prefijo dark a nuestras clases en Tailwind y de esta manera aplicar esos estilos dependiendo de si está habilitado o no el tema oscuro.

<span className="text-blue-500 dark:text-red-500 text-xl font-mono">Sumate al lado oscuro</span>
Enter fullscreen mode Exit fullscreen mode

👁️ Detectando las preferencias del usuario

Hasta el momento hardcodeamos la clase dark en la etiqueta html. Ahora vamos a ver como podemos cambiarla dinamicamente, permitirle al usuario elegir su tema preferido y persistirlo en memoria para que la próxima vez que entre al sitio se encuentre como lo dejó.

function getInitialColorMode() {
    // Checkeamos si hay guardado algo en localStorage
    // sinó usamos la preferencia del sistema oprativo del usuario
    // Agregamos o removemos la clase según sea necesario
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark')
    } else {
        document.documentElement.classList.remove('dark')
    }
}
Enter fullscreen mode Exit fullscreen mode
function MyApp({ Component, pageProps }) {
    useEffect(() => {
        getInitialColorMode()
    }, [])

    return (
        <Component {...pageProps} />
    )
}
Enter fullscreen mode Exit fullscreen mode

Incializamos nuestra aplicación, ni bien montamos el componente raíz llamamos a getInitialColorMode y getInitialColorMode setea la clase correcta en el HTML.

Hasta ahí todo viene perfecto pero hay un solo problema...

📜 SSR

Cuando navegamos a nuestra página, hacemos una petición al servidor. El servidor nos devuelve el HTML, CSS y JS que nuestro navegador va a usar para visualizar el sitio. El problema es que el servidor no tiene manera de saber la preferencia del usuario ni acceder a los valores en localStorage porque ese código se está ejecutando del lado del cliente.

El flujo es más o menos el siguiente:

  1. El cliente navega a la dirección de nuestro sitio.
  2. El servidor devuelve el HTML, CSS y JS que va a usar el cliente.
  3. El cliente carga el HTML y el CSS.
  4. Renderiza el tema por defecto del sitio.
  5. React se carga y rehidrata la página.
  6. Al correr Javascript nos damos cuenta que el usuario preferia usar el tema oscuro
  7. La página se renderiza otra vez, esta vez con el tema oscuro.

Esto nos deja un hermoso efecto de parpadeo que no queremos en nuestra web.

Parpadeo al cargar la página

¿La solución?

Injectar una etiqueta script que vaya antes del contenido principal de nuestro body

🛑 Bloqueando HTML

Para hacer esto en NextJS vamos a tener que sobreescribir _document.js.

Vamos a hacer esto cada vez que necesitemos agregar más logica a las etiquetas html y body de nuestra página.

import Document, { Html, Head, Main, NextScript } from "next/document";

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head>
            <script type="text/javascript" src='/checkUserDarkMode.js' />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
Enter fullscreen mode Exit fullscreen mode

Este es el _document.js por defecto. Lo único que estamos agregando es el que corre checkUserDarkMode.js. Este script tiene que estar ubicado en la carpeta public de nuestro proyecto y hace lo siguiente:

(function () {
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark')
    } else {
        document.documentElement.classList.remove('dark')
    }
})();
Enter fullscreen mode Exit fullscreen mode

🎣 Custom Hook

Para trabajar con el tema oscuro en nuestra aplicación podemos crear un hook que nos permita cambiar entre los distintos temas.

// useColorScheme.jsx
import { useState } from "react";

const useColorScheme = () => {
  let isDarkInitial = false
  if (typeof window !== 'undefined') {
    isDarkInitial = localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
  }
  const [isDark, setIsDark] = useState(isDarkInitial);

  const toogleTheme = () => {
    setIsDark(dark => {
      !dark
        ? document.documentElement.classList.add('dark')    
        : document.documentElement.classList.remove('dark')

      !dark
        ? localStorage.theme = 'dark'
        : localStorage.theme = 'light'
      return !dark;
    })
  }
  return { isDark, toogleTheme };
}

export default useColorScheme
Enter fullscreen mode Exit fullscreen mode

Con todo esto en su lugar ahora podemos facilmente acceder al tema que está en uso y switchear entre ellos:

import useColorScheme from '../Hooks/useColorScheme'

const { isDark, toogleTheme } = useColorScheme();

const MiComponente = () => {
    return (
        ...
        {isDark ? (
        <MoonIcon onClick={toogleTheme} />
      ) : (
        <SunIcon  onClick={toogleTheme} />
      )}
    )
}
Enter fullscreen mode Exit fullscreen mode

🔗 Links

Top comments (0)