Programar modo oscuro en Astro
Recomiendo leer Como programar el modo oscuro solo con css Se partirá de lo que alli se explica y también la idea es que la página web cargue por primera vez con el tema que tenga el usuario en el sistema operativo o navegador. Lo primero que vamos a hacer es trabajar en el html, luego en el css y por último en el javascript.
HTML
Digamos que tenemos un header donde esta el titulo de nuestra página y un menú con opciones a diferentes páginas. Queremos alli un botón para que el usuario pueda cambiar el tema. Lo vamos a hacer con un <input>
de tipo checkbox y un <label>
.
La etiqueta <label>
en HTML se utiliza para asociar un texto descriptivo a un control en un formulario web, como un campo de texto o un botón. ¿Por que usar <label>
? Porque vamos a ocultar el checkbox y el icono lo vamos a poner el esa etiqueta, de la siguiente forma:
<header>
<div class="nombre-pagina">
<a class="titulo-pagina" href="">Mi página</a>
</div>
<div class="seccion-menu">
<a class="menu" href=""> Inicio </a>
<a class="menu" href=""> Sobre mi </a>
<a class="menu" href=""> Contacto </a>
</div>
<div class="botones">
<label for="boton-tema" class="tema">
/*Aquí va el icono*/
</label>
<input type="checkbox" id="boton-tema" />
</div>
</header>
Como vemos el header se compone de tres <div>
uno para cada sección (nombre pagina, menús, botones).
En la sección de botones vemos la etiqueta label y el input. Recordar que el id
del input debe corresponder con el for
de label para que cuando le demos click a lo que pongamos dentro de <label>
este actué como si le diéramos click al input. Hasta el momento tenemos esto:
Iconos
Ahora lo que vamos a hacer es importar nuestro icono. Hay varias formas de hacer eso, se puede con svg o desde una librería de iconos externa (hasta con emojis, el problema es que no puedes cambiar de color el emoji). La forma mas sencilla es con una librería externa ya que los svg en ocasiones son difíciles de configurar. En este caso usaremos una librería externa llamada Font Awesome Para ello debemos decirle a nuestro HTML que de donde importara los iconos, para ello debemos colocar el siguiente código en nuestro head (no confundir con header)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
Esto funciona como cuando importamos una hoja de estilos (stylesheet) de css. Para usar los iconos vamos a la página web de Font Awesomey buscamos el icono que necesitamos, en este momento buscaremos una luna (moon) para representar que dando click pasará al modo oscuro, nos encontramos con el siguiente código HTML:
<i class="fa-solid fa-moon"></i>
Solo debemos colocar ese código entre nuestras etiquetas label y listo. El código quedaria de la siguiente manera.
<html>
<header>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</header>
<body>
<header>
<div class="nombre-pagina">
<a class="titulo-pagina" href="">Mi página</a>
</div>
<div class="seccion-menu">
<a class="menu" href=""> Inicio </a>
<a class="menu" href=""> Sobre mi </a>
<a class="menu" href=""> Contacto </a>
</div>
<div class="botones">
<label for="boton-tema" class="tema">
<i class="fa-solid fa-moon"></i>
</label>
<input type="checkbox" id="boton-tema" />
</div>
</header>
</body>
</html>
Como puedes ver, he completado lo que faltaba de html. Se vería así:
Usa componentes en Astro
Recuerda usar los componentes de Astro para hacerte la vida mas fácil. El header puede ir en un componente que podrás poner en tu layout base.
CSS
Bien, ahora que tenemos la estructura nos queda darle estilos. No vamos a preocuparnos por hacer la página web responsive, vamos a aplicarle estilos pensando en que solo se vera desde un computador.
Header y botón
Lo primero es saber que queremos hacer. Las pantallas de los computadores son anchas por lo que es mas cómodo angostar el texto para que los ojos no se tengan que desplazar tanto. Lo que vamos a hacer es centralizar el header, darle un padding y estilizar los tres div que componen nuestro header.
- Vamos a dar los estilos comunes en todos los proyectos: quitar el margin al body y dar la propiedad
box.sizing: border-box
para que en el tamaño de las cajas tome el borde y el padding. - A nuestro
<header>
le damos la propiedaddisplay: flex
y luego la propiedadjustify-content: space-between
. Esto hará que nuestros div se repartan el ancho del header, quedando uno en a la izquierda, otro en el centro y el otro a la derecha. - Como queremos reducir el ancho que ocupa nuestro texto le vamos a dar un padding a los lados de 10vh y, arriba y abajo de 0.5em. Con esto ya tendríamos los estilos del header, el código se vería así:
* {
box-sizing: border-box;
}
body {
margin: 0;
}
header {
display: flex;
justify-content: space-between;
padding: 0.5em 20vh;
}
Y así se vería en el navegador:
Ahora nos toca estilizar cada uno de nuestro div principales. Para los tres div quiero quitarle la decoración a los hipervínculos así que a los <a>
le damos la propiedad text-decoration: none
. Aquí le tendríamos que dar el color negro al texto pero como eso lo queremos hacer dependiendo del tema que estemos, entonces vamos a crear una variable para el color de la letra en :root
para que este disponible en todas las etiquetas, la variable la vamos a llamar --color-texto: black
. y luego la llamamos en con var(--color-texto)
,
:root {
--color-texto: black;
}
a {
text-decoration: none;
color: var(--color-texto);
}
Ahora a los menús del centro le vamos a dar un padding hacia los lados para separarlos.
Nos queda dar estilos a el botón de cambio de tema. Como queremos que solo se vea el icono, tenemos que ocultar el check, para eso le damos al input un display: none;
pero se lo damos con el id que le pusimos para que no vaya a ocultar todos los input de nuestra página web.
Luego al label le damos un width y un heigth, un borde y un padding. y centramos el texto para que el icono quede en el centro. Recuerda dar un display: block
a label para que puedas darle la propiedad de width y heigth.
.tema {
padding: 0.5em;
display: block;
border: 1px solid black;
border-radius: 20px;
width: 2em;
height: 2em;
text-align: center;
}
Se vería así: Damos por terminado el botón.
Estilos modo claro y modo oscuro
Necesitamos las variables para ambos modos con el mismo nombre, pero que cambie el valor. Ahora bien, para identificar que estamos en el modo oscuro le daremos a nuestra etiqueta html (la misma que :root) la clase dark
, cuando tenga esta etiqueta se aplicaran unos estilo, si no la tiene se aplicaran otros. entonces dentro de :root.dark
estarán las variables de color del modo oscuro y en :root
estarán las del modo claro. Luego le damos al body el color de texto y el fondo. Así quedaría:
:root {
--color-texto: black;
--color-fondo: white;
}
:root.dark {
--color-texto: white;
--color-fondo: black;
}
body {
margin: 0;
color: var(--color-texto);
background-color: var(--color-fondo);
}
Y listo, solo nos queda ir a javascript y decirle que cuando el botón este en check le aplique al html la clase dark, y cuando no este check se la quite. También haremos que el icono cambie.
Javascript
Lo primero que tenemos que hacer es seleccionar el input en una variable para que sea mas cómodo trabajar:
const boton = document.getElementById('boton-tema')
Luego queremos que cuando cambie de estado el botón haga una cosa u otra. Para esto usamos un addEvenListener con el tipo ‘change’. Para lo que tiene que hacer vamos a crear una nueva función para que sea mas fácil leerlo luego.
Lo que queremos hacer es evaluar si nuestro html tiene la clase dark
si no es así tenemos que dársela sino quitársela. Luego dependiendo del cambio le daremos los estilos, quedaría asi:
const boton = document.getElementById('boton-tema')
boton.addEventListener('change', cambioTema)
function cambioTema() {
const element = document.documentElement
const tema = element.classList.contains("dark") ? "light" : "dark"
const buttonMode = document.querySelector('#button-mode');
if (tema === "dark") {
element.classList.add("dark")
buttonMode.innerHTML = '<i class="fa-regular fa-sun icon"></i>'
} else {
element.classList.remove("dark")
buttonMode.innerHTML = '<i class="fa-solid fa-moon icon"></i>'
}
localStorage.theme = theme
}
En la constante Tema guardamos el tema actual, en esta se evalúa si el html tiene la clase dark, si es así se la quita sino se la pone. Luego se evalúa con un if si el tema quedo en “dark” si es así le da la clase dark al html, dándole los estilos y ademas cambiando el icono; si no es así le quita la clase “dark” (quitando le los estilos) y cambiando el icono.
Pero ahora, cada vez que vayamos a otra pagina el tema va a volver a claro, que es el por defecto, para ello debemos guardar el tema en la localStorage y crear una nueva función que se lanzara cada vez que se recargue la pagina y que hará prácticamente lo del if, es decir evaluar en que quedo el tema y dar los estilos.
Por eso creamos al final localStorage.theme = tema
y ponemos el light como opción en tema
. Esto guardara el tema en el navegador.
La función para que cargue el tema al inicio es la siguiente:
function cargarTema() {
const tema = (() => {
const userTema = localStorage.theme
if (userTema === "light" || userTheme === "dark") {
return userTema
} else {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
}
})()
const buttonMode = document.querySelector('#button-mode');
const element = document.documentElement
if (theme === "dark") {
element.classList.add("dark")
buttonMode.innerHTML = '<i class="fa-regular fa-sun icon"></i>'
} else {
element.classList.remove("dark")
buttonMode.innerHTML = '<i class="fa-solid fa-moon icon"></i>'
}
localStorage.theme = theme
}
document.addEventListener("astro:after-swap", cargarTema)
cargarTema();
El primer if evalúa si hay un tema guardado en el navegador, sino hay, es decir, si nunca a entrado a nuestra pagina y cambiado el tema con el botón va a buscar la preferencia de tema que tenga en el sistema o en el navegador; si el navegador esta en modo oscuro va a mostrar la página en modo oscuro, a menos que le de al botón de cambiar tema, en ese caso usara el tema que escogió y lo hará la próxima vez que entre a la página.
El segundo if es el mismo que el de la función cambiar tema pues cada página tiene que cargar el tema cada vez que se recarga o sino se va a recargar siempre con el tema claro.
La última parte es una configuración para Astro.
Al final se ejecuta la función.
Configuración para astro
Para que en Astro se cargue antes de que se cargue la pagina se debe usar un addEventListener especial, que es el siguiente
document.addEventListener("astro:after-swap", cargarTema)
Esto hará que cuando se cargue una página se ejecute esa función antes de que se muestre la página, así no se vera cambiar el tema de claro a oscuro por una milésima de segundo, cosa que no se quiere. Y listo.