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'
...
}
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>
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>
👁️ 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')
}
}
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:
- El cliente navega a la dirección de nuestro sitio.
- El servidor devuelve el HTML, CSS y JS que va a usar el cliente.
- El cliente carga el HTML y el CSS.
- Renderiza el tema por defecto del sitio.
- React se carga y rehidrata la página.
- Al correr Javascript nos damos cuenta que el usuario preferia usar el tema oscuro
- 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.
¿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;
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')
}
})();
🎣 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
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} />
)}
)
}
Top comments (0)