:has(): el selector de familia

Desde que comenzó la época (en términos de CSS), hemos trabajado con una cascada en varios sentidos. Nuestros estilos componen una "Hoja de estilo en cascada". Y nuestros selectores también se muestran en cascada. Pueden moverse de lado. En la mayoría de los casos, disminuyen. Pero nunca hacia arriba. Durante años, soñamos con un "selector de padres". ¡Ya llegó, por fin! Tiene la forma de un seudoselector :has().

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

Pero es más que un "padre" selector. Esa es una buena manera de comercializarla. La forma no tan atractiva podría ser el "entorno condicional" selector. Pero eso no es el mismo. ¿Qué pasa con la "familia" selector?

Navegadores compatibles

Antes de continuar, vale la pena mencionar la compatibilidad con el navegador. Aún no está ahí. Pero se está acercando. Por el momento, no es compatible con Firefox, por lo que estamos planeando hacerlo. Sin embargo, ya está en Safari y se lanzará en Chromium 105. En todas las demostraciones de este artículo, se indicará si el navegador utilizado no las admite.

Cómo usar la función :has

¿Cómo es eso? Considera el siguiente HTML con dos elementos del mismo nivel con la clase everybody. ¿Cómo seleccionarías el que tiene un elemento subordinado 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;
}

De esta manera, se selecciona la primera instancia de .everybody y se aplica un animation.

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

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

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

Selecciona elementos figure que tengan un figcaption directo. css figure:has(> figcaption) { ... } Selecciona anchor que no tengan un subordinado directo de SVG css a:not(:has(> svg)) { ... } Selecciona los label que tengan un hermano input directo. ¡Hacia los costados! css label:has(+ input) { … } Selecciona articles donde un img subordinado no tenga texto alt css article:has(img:not([alt])) { … } Selecciona el elemento 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)) { ... } Seleccionar todos los elementos de una cuadrícula sobre los que no se coloca el cursor 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 solo a dentro de un párrafo que tenga un elemento del mismo nivel hr css p:has(+ hr) a:only-child { … } Selecciona una article en la que se cumplan varias condiciones css article:has(>h1):has(>h2) { … } Mezcla eso. Selecciona una article en la que un título esté seguido de un subtítulo. css article:has(> h1 + h2) { … } Selecciona :root cuando se activen los estados interactivos css :root:has(a:hover) { … } Selecciona el párrafo que sigue a una figure que no tenga un figcaption. css figure:not(:has(figcaption)) + p { … }

¿Qué casos de uso interesantes se te ocurren para :has()? Lo fascinante es que te motiva a romper tu modelo mental. Te hace pensar: "¿Podrías abordar estos estilos de otra manera?".

Ejemplos

Veamos algunos ejemplos de cómo podemos usarlo.

Tarjetas

Haz una demostración con una tarjeta clásica. Podríamos mostrar cualquier información en nuestra tarjeta, como un título, subtítulo o 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 presentar contenido multimedia? Para este diseño, la tarjeta podría dividirse en dos columnas. Antes, podías crear una clase nueva para representar este comportamiento, por ejemplo, card--with-media o card--two-columns. Estos nombres de clases no solo se vuelven difíciles de evocar, sino que también son difíciles de mantener y recordar.

Con :has(), puedes detectar si la tarjeta tiene contenido multimedia y hacer lo correcto. No se necesitan nombres de clase de modificador.

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

No es necesario que lo dejes allí. Podrías usar tu creatividad. ¿Cómo podría adaptarse una tarjeta que muestra contenido "destacado" en un diseño? Este CSS convertiría una tarjeta destacada en el ancho completo del diseño y la colocaría 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 destacada 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;
}

Muchas posibilidades.

Formularios

¿Y los formularios? Se los conoce por ser difíciles de diseñar. Un ejemplo de esto es aplicar diseño a las entradas y sus etiquetas. Por ejemplo, ¿cómo señalamos que un campo es válido? Con :has(), esto es mucho más fácil. Podemos incluir las seudoclases del formato correspondiente, 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 o 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, ocultas el mensaje de error.

.form-group__error {
  display: none;
}

Sin embargo, cuando el campo pasa a ser :invalid y no está enfocado, puedes mostrar el mensaje sin la necesidad de nombres de clase adicionales.

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

No hay razón para que no puedas agregar un toque de buen gusto a la hora en que los usuarios interactúan con tu formulario. Considera el siguiente ejemplo. Observe cuando ingrese un valor válido para la microinteracción. Un valor :invalid hará que el grupo de formularios se mueva. Pero 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 el flujo de tu documento? Promueve ideas sobre cómo podríamos dar estilo a la tipografía en torno al contenido multimedia, por ejemplo.

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 una figcaption presente, ocupan todo el ancho y obtienen un margen adicional.

Cómo reaccionar al estado

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

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

¿Puede ayudar a evitar errores del usuario?

¿Qué tienen en común todos estos ejemplos? Además de mostrar formas de usar :has(), ninguna de ellas requería modificar los nombres de las clases. Cada uno de ellos insertó contenido nuevo y actualizó un atributo. Este es un gran beneficio de :has(), ya que puede ayudar a mitigar el error del usuario. Con :has(), CSS puede asumir la responsabilidad de ajustarse a las modificaciones en el DOM. No es necesario que hagas malabares con los nombres de las clases en JavaScript, lo que reduce las posibilidades de que se produzcan errores de los desarrolladores. A todos nos pasa cuando escribimos un nombre de clase de forma incorrecta y tenemos que recurrir a mantenerlos en las búsquedas de Object.

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

Pensar de manera creativa

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

<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. Podrías usar eso para desviar un formulario con transformaciones. Ten en cuenta que esta demostración se ve mejor en una pestaña independiente del navegador.

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

Aunque no los pondrás en producción en un futuro inmediato, 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 surgen debido a los hits del rendimiento.

  • No puedes :has() un :has(). Sin embargo, puedes encadenar un :has(). css :has(.a:has(.b)) { … }
  • No se usan seudoelementos en :has() css :has(::after) { … } :has(::first-letter) { … }
  • Restringe el uso de :has() dentro de 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 seudoelemento 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 esta Glitch. Dale crédito a Byungwoo por compartir estas estadísticas y detalles sobre la implementación.

Eso es todo.

Prepárate para :has(). Cuéntales a tus amigos y comparte esta publicación. Será un gran cambio en nuestro enfoque de las CSS.

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