:has(): el selector de familia

Desde el principio de los tiempos (en términos de CSS), hemos trabajado con una cascada en varios sentidos. Nuestros estilos componen una "Hoja de estilos en cascada". Nuestros selectores también se organizan en cascada. Pueden ir hacia los lados. En la mayoría de los casos, se dirigen hacia abajo. Pero nunca hacia arriba. Durante años, soñamos con un "Selector de elementos superiores". Y ahora, por fin, está disponible. En forma de un seudoselector :has().

La pseudoclase CSS :has() representa un elemento si alguno de los selectores pasados como parámetros coincide con al menos un elemento.

Sin embargo, es más que un selector "superior". Es una buena forma de comercializarlo. La forma menos atractiva podría ser el selector de "entorno condicional". Pero no suena igual. ¿Qué sucede con el selector "familia"?

Navegadores compatibles

Antes de continuar, vale la pena mencionar la compatibilidad con navegadores. Aún no está disponible. Pero se está acercando. Aún no se admite Firefox, pero está en la planificación. Sin embargo, ya está disponible en Safari y se lanzará en Chromium 105. Todas las demostraciones de este artículo te indicarán si no son compatibles con el navegador que usas.

Cómo usar :has

¿Cómo es eso? Considera el siguiente código HTML con dos elementos hermanos con la clase everybody. ¿Cómo seleccionarías el que tiene un descendiente con la clase a-good-time?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

Con :has(), puedes hacerlo con el siguiente CSS.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

Esto selecciona la primera instancia de .everybody y aplica un animation.

En este ejemplo, el elemento con la clase everybody es el objetivo. La condición es tener un descendiente con la clase a-good-time.

<target>:has(<condition>) { <styles> }

Sin embargo, puedes ir mucho más allá, ya que :has() abre muchas oportunidades. Incluso aquellos que aún no se descubrieron. Considera algunas de estas opciones.

Selecciona los elementos figure que tengan un figcaption directo. css figure:has(> figcaption) { ... } Selecciona los anchor que no tienen un descendiente SVG directo. css a:not(:has(> svg)) { ... } Selecciona los label que tienen un hermano input directo. ¡Vamos hacia los lados! css label:has(+ input) { … } Selecciona article en los que un img descendiente no tenga texto alt css article:has(img:not([alt])) { … } Selecciona el documentElement en el que haya algún estado presente en el DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Selecciona el contenedor de diseño con un número impar de elementos secundarios css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Selecciona todos los elementos de una cuadrícula que no tengan el cursor sobre ellos css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Selecciona el contenedor que contiene un elemento personalizado <todo-list> css main:has(todo-list) { ... } Selecciona cada a único dentro de un párrafo que tenga un elemento hr hermano directo css p:has(+ hr) a:only-child { … } Selecciona un article en el que se cumplan varias condiciones css article:has(>h1):has(>h2) { … } Combina todo esto. Selecciona un article en el que un título esté seguido de un subtítulo.css article:has(> h1 + h2) { … }Selecciona el :root cuando se activen los estados interactivos.css :root:has(a:hover) { … }Selecciona el párrafo que sigue a un figure que no tiene un figcaption.css figure:not(:has(figcaption)) + p { … }

¿Puedes pensar en casos de uso interesantes para :has()? Lo fascinante aquí es que te anima a romper tu modelo mental. Te hace pensar: "¿Podría abordar estos estilos de otra manera?".

Ejemplos

Veamos algunos ejemplos de cómo podríamos usarlo.

Tarjetas

Realiza una demostración de la tarjeta clásica. Podríamos mostrar cualquier información en nuestra tarjeta, por ejemplo, un título, un subtítulo o algún contenido multimedia. Esta es la tarjeta básica.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

¿Qué sucede cuando quieres agregar contenido multimedia? Para este diseño, la tarjeta podría dividirse en dos columnas. Antes, podrías crear una clase nueva para representar este comportamiento, por ejemplo, card--with-media o card--two-columns. Estos nombres de clase no solo se vuelven difíciles de recordar, sino también de mantener.

Con :has(), puedes detectar que la tarjeta tiene contenido multimedia y hacer lo correcto. No es necesario usar nombres de clases de modificadores.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

Y no es necesario que lo dejes allí. Puedes ser creativo con ella. ¿Cómo podría adaptarse una tarjeta que muestra contenido “destacado” dentro de un diseño? Este CSS haría que una tarjeta de contenido destacado tenga el ancho completo del diseño y la coloque al comienzo de una cuadrícula.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

¿Qué sucede si una tarjeta de contenido destacado con un banner se mueve para llamar la atención?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

Tantas posibilidades.

Formularios

¿Qué sucede con los formularios? Se sabe que son difíciles de peinar. Un ejemplo de esto es aplicar diseño a las entradas y sus etiquetas. ¿Cómo indicamos que un campo es válido, por ejemplo? Con :has(), esto se vuelve mucho más fácil. Podemos conectarnos a las pseudoclases de formulario relevantes, por ejemplo, :valid y :invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

Pruébalo en este ejemplo: ingresa valores válidos y no válidos, y activa y desactiva el enfoque.

También puedes usar :has() para mostrar y ocultar el mensaje de error de un campo. Toma nuestro grupo de campos “correo electrónico” y agrégale un mensaje de error.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

De forma predeterminada, se oculta el mensaje de error.

.form-group__error {
  display: none;
}

Sin embargo, cuando el campo se convierte en :invalid y no está enfocado, puedes mostrar el mensaje sin necesidad de nombres de clase adicionales.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

No hay razón para no agregar un toque de capricho cuando los usuarios interactúan con tu formulario. Considera el siguiente ejemplo. Observa cuando ingreses un valor válido para la microinteracción. Un valor :invalid hará que el grupo de formularios tiemble. Sin embargo, solo si el usuario no tiene preferencias de movimiento.

Contenido

Hablamos de esto en los ejemplos de código. Pero, ¿cómo podrías usar :has() en tu flujo de documentos? Por ejemplo, arroja ideas sobre cómo podríamos aplicar diseño a la tipografía alrededor del contenido multimedia.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

Este ejemplo contiene figuras. Cuando no tienen figcaption, flotan dentro del contenido. Cuando hay un figcaption, ocupa el ancho completo y obtiene un margen adicional.

Cómo reaccionar ante el estado

¿Qué tal si haces que tus estilos sean reactivos a algún estado en nuestro lenguaje de marcado? Considera un ejemplo con la barra de navegación deslizante "clásica". Si tienes un botón que activa o desactiva la apertura del panel de navegación, es posible que use el atributo aria-expanded. Se podría usar JavaScript para actualizar los atributos adecuados. Cuando aria-expanded sea true, usa :has() para detectarlo y actualizar los estilos del navegador deslizante. JavaScript hace su parte y CSS puede hacer lo que quiera con esa información. No es necesario reorganizar el marcado ni agregar nombres de clases adicionales, etcétera. (Nota: Este no es un ejemplo listo para producción).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

¿Puede :has ayudar a evitar errores del usuario?

¿Qué tienen en común todos estos ejemplos? Además del hecho de que muestran formas de usar :has(), ninguno de ellos requirió modificar los nombres de las clases. Cada uno insertó contenido nuevo y actualizó un atributo. Este es un gran beneficio de :has(), ya que puede ayudar a mitigar los errores del usuario. Con :has(), CSS puede asumir la responsabilidad de adaptarse a las modificaciones en el DOM. No es necesario que combines nombres de clases en JavaScript, lo que reduce las posibilidades de error del desarrollador. Todos hemos pasado por eso cuando escribimos mal un nombre de clase y tenemos que recurrir a mantenerlo en las búsquedas de Object.

Es una idea interesante. ¿Nos lleva a un marcado más limpio y a menos código? Menos JavaScript, ya que no estamos realizando tantos ajustes de JavaScript. Menos código HTML, ya que ya no necesitas clases como card card--has-media, etcétera.

Piensa de forma creativa

Como se mencionó anteriormente, :has() te anima a romper el modelo mental. Es una oportunidad para probar diferentes cosas. Una de esas formas de intentar superar los límites es crear mecánicas de juego solo con CSS. Por ejemplo, podrías crear una mecánica basada en pasos con formularios y CSS.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

Y eso abre posibilidades interesantes. Puedes usarlo para recorrer un formulario con transformaciones. Ten en cuenta que esta demostración se ve mejor en una pestaña del navegador independiente.

Y para divertirte, ¿qué tal el clásico juego de cables electrificados? La mecánica es más fácil de crear con :has(). Si el cursor se coloca sobre el cable, el juego termina. Sí, podemos crear algunas de estas mecánicas de juego con elementos como los combinatores de hermanos (+ y ~). Sin embargo, :has() es una forma de lograr esos mismos resultados sin tener que usar "trucos" de marcado interesantes. Ten en cuenta que esta demostración se ve mejor en una pestaña del navegador independiente.

Si bien no las lanzarás a producción pronto, destacan las formas en que puedes usar la primitiva. Por ejemplo, poder encadenar un :has().

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

Rendimiento y limitaciones

Antes de irnos, ¿qué no puedes hacer con :has()? Existen algunas restricciones con :has(). Los principales se deben a los hits de rendimiento.

  • No puedes :has() un :has(). Sin embargo, puedes encadenar un :has(). css :has(.a:has(.b)) { … }
  • No se usa ningún elemento pseudo dentro de :has(). css :has(::after) { … } :has(::first-letter) { … }
  • Se restringió el uso de :has() dentro de los pseudos que solo aceptan selectores compuestos. css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Restringe el uso de :has() después del elemento pseudo css ::part(foo):has(:focus) { … }
  • El uso de :visited siempre será falso.css :has(:visited) { … }

Para ver las métricas de rendimiento reales relacionadas con :has(), consulta este error. Agradecemos a Byungwoo por compartir estas estadísticas y detalles sobre la implementación.

Eso es todo.

Prepárate para :has(). Cuéntale a tus amigos y comparte esta publicación. Cambiará la forma en que abordamos el CSS.

Todas las demostraciones están disponibles en esta colección de CodePen.