:has(): el selector de familia

Desde el comienzo (en términos de los CSS), hemos trabajado con una cascada en varios sentidos. Nuestros estilos componen una "Hoja de estilo en cascada". Y nuestros selectores también se transmiten en cascada. Pueden ir de lado. En la mayoría de los casos, bajan. Pero nunca hacia arriba. Durante años, hemos soñado con un "selector principal". ¡Por fin estará disponible! En 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.

Sin embargo, es más que un selector "principal". Esa es una buena forma de comercializarla. La forma no tan atractiva podría ser el selector de “entorno condicional”. Pero no es exactamente el mismo tono. ¿Y el selector de "familia"?

Navegadores compatibles

Antes de continuar, vale la pena mencionar la compatibilidad con los navegadores. Aún no está allí. Pero está cada vez más cerca. Firefox aún no es compatible, está en proceso de agregar. Sin embargo, ya está 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 utilizado.

Cómo usar :has

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

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

En este ejemplo, el elemento con la clase everybody es el destino. 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á porque :has() ofrece muchas oportunidades. Incluso es probable que aún no se descubran algunos. Piensa en algunas de estas.

Selecciona elementos figure que tengan un figcaption directo. css figure:has(> figcaption) { ... } Selecciona objetos anchor que no tengan un subordinado de SVG directo. css a:not(:has(> svg)) { ... } Selecciona los objetos label que tengan un elemento secundario directo de input. ¡Va de costado! css label:has(+ input) { … } Selecciona los elementos article en los que un elemento img subordinado no tenga texto alt. css article:has(img:not([alt])) { … } Selecciona el elemento documentElement en el que algún estado esté 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 estén colocados. 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) { ... } Seleccionar cada solo a donde haya un solo a dentro de un párrafo que tenga un sib directo.articlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Selecciona una article donde el 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 la figure que no tenga un figcaption. css figure:not(:has(figcaption)) + p { … }

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

Ejemplos

Veamos algunos ejemplos de cómo podemos usarlo.

Tarjetas

Haz una demostración de tarjetas 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 presentar algunos medios? 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 se vuelven 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 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>

No es necesario que lo dejes allí. Podrías ser creativo. ¿Cómo se podría adaptar dentro de un diseño una tarjeta que muestra contenido "destacado"? Este CSS haría que una tarjeta destacada ocupe todo el ancho 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 el estilo de 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 conectarnos con las seudoclases de formato 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 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 "email" 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, 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 la 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 especial de buen gusto a cuando los usuarios interactúan con el 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 se sacude. Pero solo si el usuario no tiene preferencias de movimiento.

Contenido

Analizamos esto en los ejemplos de códigos. Sin embargo, ¿cómo podrías usar :has() en tu flujo de documentos? Arroja ideas sobre cómo podríamos diseñar la tipografía en torno a los medios, 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 cifras. Cuando no tienen figcaption, flotan dentro del contenido. Cuando hay un elemento figcaption, este ocupa el ancho completo y obtiene un margen adicional.

Cómo reaccionar al estado

Te recomendamos que hagas que tus estilos reaccionen 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 opción para abrir el navegador, es posible que se use 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 de la navegación deslizante. JavaScript hace su parte, y CSS puede hacer lo que quiera con esa información. No es necesario modificar el lenguaje de marcado ni agregar nombres de clase adicionales, etc. (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? Aparte de que muestran formas de usar :has(), ninguna de ellas requería la modificación de los nombres de clase. 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 ajustarse a las modificaciones del DOM. No es necesario hacer malabares con los nombres de las clases en JavaScript, lo que disminuye las posibilidades de que ocurran errores del desarrollador. Todos estuvimos ahí cuando escribimos el nombre de una clase 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 hacemos tantos ajustes de JavaScript. Menos HTML, ya que no necesitas clases como card card--has-media, etcétera.

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 maneras de tratar de superar los límites es crear 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 otra pestaña del navegador.

Y por diversión, ¿qué te parece el clásico juego de alambre buzz? La mecánica es más fácil de crear con :has(). Si se coloca el cursor sobre el cable, significa que terminó el juego. 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" de lenguaje de marcado interesantes. Ten en cuenta que esta demostración se ve mejor en otra pestaña del navegador.

Aunque no las pondrás en producción en ningún momento, destacan las formas en que puedes usar las primitivas. Por ejemplo, puedes 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()? :has() tiene algunas restricciones. 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 hay uso de seudoelementos dentro de :has() css :has(::after) { … } :has(::first-letter) { … }.
  • Restringir 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)) { … }
  • Restringir el uso de :has() después del seudoelemento css ::part(foo):has(:focus) { … }
  • El uso de :visited siempre será falso. css :has(:visited) { … }

Para obtener métricas de rendimiento reales relacionadas con :has(), consulta esta Glitch. Crédito a Byungwoo por compartir estas estadísticas y detalles sobre la implementación.

Eso es todo.

Prepárate para el :has(). Cuéntales a tus amigos y comparte esta publicación, ya que cambiará las reglas del enfoque de CSS.

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