Alternar a modo oscuro solo con CSS

Publicado por Albert Escamilla el

FrontCSSdarkoscuro

Ha sido un sueño largo para muchos de los desarrolladores front-end tener una forma de aplicar CSS a un elemento en función de lo que sucede dentro de ese elemento. Es por eso que me hace especial ilusión empezar a escribir este artículo y mostraros como de potente es el siguiente concepto que os voy a explicar.

Tal vez queramos aplicar un diseño a un elemento de `<article>` si hay una imagen principal en la parte superior y un diseño diferente si no hay una imagen principal. O tal vez queramos aplicar diferentes estilos a un formulario dependiendo del estado de uno de sus campos de entrada. ¿Qué tal darle a una barra lateral un color de fondo si hay un determinado componente en esa barra lateral y un color de fondo diferente si ese componente no está presente? Los casos de uso como estos nos los hemos encontrado muchas veces.

Pues finalmente aparece en nuestras vidas un selector llamado `:has()`, el cual, se va a encargar de seleccionar el padre de un elemento hijo.

Entonces, echemos un vistazo práctico paso a paso a lo que los desarrolladores pueden hacer con este selector tan deseado.

Cómo usar :has() como selector principal

Empecemos con lo básico. Imaginad que queremos diseñar un `<figure>` según el tipo de contenido. A veces nuestro figure envuelve solo una imagen.

<figure>
  <img src="images.jpg" alt="texto alternativo">
</figure>

Mientras que otras veces hay una imagen con un pie de foto.

<figure>
  <img src="imagen.jpg" alt="texto alternativo">
  <figcaption>Esto es el pie de foto</figcaption>
</figure>

Ahora vamos a aplicar algunos estilos que solo se aplicarán si hay un figcaption dentro del figure.

figure:has(figcaption) {
  background: white;
  padding: 0.6rem;
}

Este selector significa que se seleccionará cualquier figure que tenga un figcaption en el interior.

Usando :has() con el combinador hijo

Primero, una revisión rápida de la diferencia entre el combinador descendiente y el combinador hijo (>).

El combinador descendiente ha existido desde el comienzo de CSS. Simplemente, es separar el padre y el hijo con un espacio:

a img { ... }

Esto apunta a todos los `<img>` que están contenidos dentro de un `<a>`, sin importar qué tan separados estén el `<a>` y el `<img>` en el DOM.

<a>
  <figure>
    <img src="imagen.jpg" alt="texto alternativo">
  </figure>
</a>

El combinador hijo es el nombre que se da cuando colocamos un (>) entre dos selectores, lo que le dice al navegador que apunte a cualquier cosa que coincida con el segundo selector, pero solo cuando el segundo selector es un elemento directo del primero.

a > img { ... }

Por ejemplo, este selector apunta a todos los `<img>` envueltos por un `<a>`, pero solo cuando `<img>` está inmediatamente después en el árbol DOM.

<a>
  <img src="imagen.jpg" alt="texto alternativo">
</a>

Con eso en mente, consideremos la diferencia entre los siguientes dos ejemplos. Ambos seleccionan el `<a>`, en lugar del `<img>`, ya que estamos usando `:has()`.

a:has(img) { ... }
a:has(> img) { ... }

El primero selecciona cualquier `<a>` con un `<img>` interior en cualquier lugar de la estructura HTML. Mientras que el segundo selecciona un elemento solo si `<img>` es un elemento secundario directo del `<a>`.

Usando :has() con combinadores de hermanos

Repasemos los dos selectores con relaciones de hermanos. Está el combinador del hermano (+) y el combinador del hermano (~).

h2 + p

En el ejemplo, el combinador del hermano siguiente (+) selecciona solo los párrafos que vienen directamente después de un h2.

<h2>Titulo</h2>
<p>Paragraph that is selected by `h2 + p`, because it's directly after `h2`.</p>

El combinador de hermanos posteriores (~) selecciona todos los párrafos que vienen después de un h2. Deben ser hermanos, pero puede haber cualquier número de otros elementos HTML en el medio.

h2 ~ p
<h2>Headline</h2>
<h3>Something else</h3>
<p>Paragraph that is selected by `h2 ~ p`.</p>
<p>This paragraph is also selected.</p>

Tened en cuenta que ambos `h2 + p` y `h2 ~ p` seleccionan los elementos del párrafo, y no los h2. Al igual que otros selectores (piense en `a img`), el selector apunta al último elemento de la lista. Pero, ¿y si queremos apuntar al `h2`? Podemos usar combinadores hermanos con `:has()`.

h2:has(+ p) { 
  margin-bottom: 0; 
}

¿Qué pasa si queremos hacer esto para los seis elementos del título, sin escribir seis copias del selector? Podemos usar `:is` para simplificar nuestro código.

:is(h1, h2, h3, h4, h5, h6):has(+ p) { 
  margin-bottom: 0;
}

¿O qué pasa si queremos escribir este código para más elementos que solo párrafos? Eliminemos el margen inferior de todos los titulares siempre que vayan seguidos de párrafos, subtítulos, ejemplos de código y listas.

:is(h1, h2, h3, h4, h5, h6):has(+ :is(p, figcaption, pre, dl, ul, ol)) { 
  margin-bottom: 0;
}

La combinación `:has()` con combinadores de descendientes, combinadores de hijos (>), combinadores de hermanos siguientes (+) y combinadores de hermanos posteriores (~) abre un mundo de posibilidades.

Ejemplo de modo oscuro sin JavaScript

Siempre que haya una oportunidad de usar CSS en lugar de JavaScript, la tenemos que aprovechar. Esto da como resultado una experiencia más rápida y un sitio web más sólido. JavaScript puede hacer cosas asombrosas y deberíamos usarlo cuando sea la herramienta adecuada para el trabajo. Pero si podemos lograr el mismo resultado solo en HTML y CSS, eso es aún mejor.

Sabiendo esto, lo que os quiero es explicar es un modo fácil y sencillo de crear un alternador de modo oscuro sin usar JavaScript. Para crear este alternador usaremos un checkbox para que el usuario pueda alternar entre el modo oscuro y modo claro.

<div class="container">
  <div class="switch">
    <input type="checkbox" id="check">
    <label for="check">Check to switch dark mode</label>
  </div>
  
  <div class="card color-card">
    <img
      src="https://images.unsplash.com/photo-1499557354967-2b2d8910bcca?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=7d5363c18112a02ce22d0c46f8570147&auto=format&fit=crop&w=635&q=80%20635w"
    />

    <h1 class="title">Beverly Little</h1>

    <span class="job-title">SENIOR PRODUCT DESIGNER</span>

    <p>Create usable interface and designs @BeverlyLittle</p>

    <button class="button">Hire me</button>

    <div class="followers">
      <div>
        <h2 class="title">12.3k</h2>
        <p class="followers">Followers</p>
      </div>
      <div class="grid-2">
        <h2 class="title">16k</h2>
        <p class="followers">Followers</p>
      </div>
      <div class="grid-2">
        <h2 class="title">17.8k</h2>
        <p class="followers">Followers</p>
      </div>
    </div>
  </div>
</div>
.container {
	margin: 0 auto;
	padding: 0px;
	position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  flex-direction: column;
}

.switch {
  margin-bottom: 2rem;
  display: flex;
  align-items: center;
}

.switch input {
  width: 1rem;
  height: 1rem;
  margin-right: 0.5rem;
}

.card {
  border-radius: 6px;
  display: inline-block;
  position: relative;
  width: 375px;
  box-shadow: 0 12px 13px rgb(0 0 0 / 16%), 0 12px 13px rgb(0 0 0 / 16%);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 50px;
  color: #31394D;
  background-color: #FFF;
}

.card .title {
  font-size: 2rem;
}

.card .job-title {
  display: block;
  margin-bottom: 1.5rem;
  font-size: 0.8rem;
}

.card p {
  margin-bottom: 1.5rem;
}

.card button {
  padding: 0.8rem 2rem;
  margin-bottom: 1.5rem;
  border-radius: 999px;
  color: #FFF;
  background-color: #31394D;
}

.card img {
  max-width: 100%;
  border-radius: 100%;
  width: 132px;
  height: 128px;
  margin-bottom: 50px;
}

.card .followers {
  display: flex;
  justify-content: space-between;
  width: 100%;
}

.card .followers > div {
  display:flex;
  align-items: center;
  flex-direction: column;
}

.card .followers > div .title {
  font-size: 1.5rem;
}

Cómo podéis ver hemos creado un card y un checbox que hemos estilado con CSS para el ejemplo. Una vez tengamos esto usaremos lo aprendido con el selector `:has()` para hacer el alternador.

body:has(input:checked) .card {
  color: #FFF;
  background-color: #31394D;
}

body:has(input:checked) .card button {
  color: #31394D;
  background-color: #FFF;
}

Usaremos `body:has(input:checked)` para observar cuando el checkbox está en modo checked y cuando no. A partir de aquí podrás seleccionar cualquier elemento para poder modificar su aspecto. En el ejemplo uso este selector para modificar el color de fondo y texto de la card, así como su botón interior.

La revolución del `:has()`

Esto se siente como una revolución en la forma en que escribiremos selectores de CSS, abriendo un mundo de posibilidades que antes eran imposibles o que a menudo no valían la pena. Parece que si bien podemos reconocer de inmediato cuán útil es :has(), tampoco tenemos idea de lo que es realmente posible. En los próximos años, las personas que hacen demostraciones y se sumergen profundamente en lo que CSS puede hacer, generarán ideas sorprendentes, que llevarán :has() a sus límites.

La parte más difícil de :has() será abrir nuestras mentes a sus posibilidades. Nos hemos acostumbrado tanto a los límites que se nos imponen al no tener un selector de este tipo que ahora tenemos que romper con nuestros hábitos.

Así que ¿Cómo usaréis vosotros :has()?

Autor

Albert Escamilla

Maquetador Frontend UI en knowmad mood con una gran sensibilidad para el diseño, que además, tiene como obsesión mantener el código limpio y ordenado.