Dark mode con TailwindCSS y NextJS
Tags: tailwindcss, nextjs, ssr, darkmode
Published: 2021-07-22 12:31:15

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`

```jsx
module.exports = {
	...
  darkMode: 'class'
	...
}
```

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

```html
<html className="dark">
	....
</html>
```

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.

```html
<span className="text-blue-500 dark:text-red-500 text-xl font-mono">Sumate al lado oscuro</span>
```

## 👁️ 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ó.

```jsx
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')
	}
}
```

```jsx
function MyApp({ Component, pageProps }) {
	useEffect(() => {
		getInitialColorMode()
	}, [])

	return (
		<Component {...pageProps} />
	)
}
```

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](https://ericluna.dev/_next/image?url=%2Fblog%2Fparpadeo_tema_oscuro.GIF&w=1920&q=75)

¿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.

```jsx
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;
```

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

```jsx
(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')
    }
})();
```

## 🎣 Custom Hook

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

```jsx
// 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
```

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

```jsx
import useColorScheme from '../Hooks/useColorScheme'

const { isDark, toogleTheme } = useColorScheme();

const MiComponente = () => {
	return (
		...
		{isDark ? (
	    <MoonIcon onClick={toogleTheme} />
	  ) : (
	    <SunIcon  onClick={toogleTheme} />
	  )}
	)
}
```
 

## 🔗 Links

- 📖 [Custom Document en NextJS](https://nextjs.org/docs/advanced-features/custom-document)
- 📖 [Dark mode de TailwindCSS](https://tailwindcss.com/docs/dark-mode)