tutoriales.com

Creando Componentes Interactivos con la Pseudo-Clase :has() de CSS

Descubre cómo la pseudo-clase `:has()` de CSS revoluciona la forma en que interactuamos con el DOM. Este tutorial te guiará a través de ejemplos prácticos para crear diseños condicionales y componentes interactivos, mejorando la modularidad de tu CSS. Explorarás su sintaxis, casos de uso y las implicaciones de su soporte en navegadores.

Intermedio15 min de lectura8 views
Reportar error

La pseudo-clase :has() es una de las adiciones más potentes y esperadas a CSS en los últimos años. Con ella, finalmente podemos seleccionar elementos basándonos en sus descendientes o hermanos, abriendo un mundo de posibilidades para la creación de componentes interactivos y diseños condicionales puramente con CSS. ¡Adiós a muchos trucos de JavaScript y clases auxiliares innecesarias!

📖 ¿Qué es la Pseudo-Clase :has()?

En pocas palabras, :has() permite que un selector 'mire hacia atrás' o 'mire a los lados' en el árbol del DOM. Tradicionalmente, CSS solo nos ha permitido seleccionar elementos hijos o posteriores a otro elemento. Por ejemplo, ul li selecciona un li dentro de un ul. Pero con :has(), puedes seleccionar el ul porque tiene un li específico, o un div porque contiene una imagen, o incluso un input que está deshabilitado.

La sintaxis básica es selector:has(relativo_selector). El selector será el elemento que se estiliza si la condición dentro de :has() se cumple.

💡 Consejo: Piensa en `:has()` como un 'selector padre' o un 'selector anterior' que reacciona a los cambios en sus hijos o hermanos. Esto es una verdadera revolución en CSS.

🚀 Ventajas Clave de :has()

  • Mayor control de estilo: Permite estilos condicionales basados en el contenido o estado de los hijos. Ejemplo: estilizar una tarjeta solo si tiene una imagen destacada.
  • Menos JavaScript: Reduce la necesidad de JavaScript para manipulaciones de clases basadas en el estado del DOM.
  • CSS más limpio y modular: Permite crear componentes más autocontenidos y menos acoplados a la estructura del HTML.
  • Mejor accesibilidad: Al reducir el JavaScript para el manejo de estado, se puede mejorar la accesibilidad y el rendimiento.

⚙️ Sintaxis y Uso Básico

La sintaxis de :has() es bastante flexible y puede combinarse con otros selectores CSS. El argumento de :has() es una lista de selectores relativos, que pueden incluir combinadores (espacio, +, ~, >).

selector-a:has(selector-relativo-b)

Esto selecciona selector-a si al menos uno de los selector-relativo-b dentro de su subárbol de elementos coincide.

Ejemplo Básico: Estilizando un Padre Basado en un Hijo

Imagina que quieres dar un borde rojo a cualquier div que contenga una imagen.

<div class="card">
  <p>Esta tarjeta no tiene imagen.</p>
</div>

<div class="card">
  <img src="imagen.jpg" alt="Descripción de imagen">
  <p>Esta tarjeta sí tiene imagen.</p>
</div>
.card:has(img) {
  border: 2px solid red;
}

En este caso, solo la segunda tarjeta tendrá el borde rojo. ¡Fantástico!

Funcionamiento de CSS :has() div.card (Nodo Padre) :has(img) Pseudo-clase relacional ¿Existe <img> dentro del div? SÍ (Verdadero) Se aplica el estilo al Padre (div.card) div.card { border-color: green; } <img> (Hijo)

💡 Casos de Uso Prácticos y Ejemplos Avanzados

Exploremos algunos escenarios donde :has() brilla con luz propia.

1. Estilos Condicionales para Componentes UI

Consideremos un componente de tarjeta que puede tener un botón de acción o no. Queremos que la tarjeta ocupe el ancho completo si no tiene botón, y la mitad si sí lo tiene.

<div class="container">
  <div class="product-card">
    <h2>Producto A</h2>
    <p>Descripción breve del producto A.</p>
    <button>Comprar</button>
  </div>

  <div class="product-card">
    <h2>Producto B</h2>
    <p>Descripción breve del producto B.</p>
  </div>

  <div class="product-card">
    <h2>Producto C</h2>
    <p>Descripción breve del producto C.</p>
    <button>Añadir al carrito</button>
  </div>
</div>
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
}

.product-card {
  border: 1px solid #eee;
  padding: 15px;
  border-radius: 8px;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Si la tarjeta tiene un botón, queremos que ocupe menos espacio o tenga un estilo diferente */
.product-card:has(button) {
  background-color: #e0f7fa; /* Un color diferente para destacar */
  border-color: #00bcd4;
}

/* Podemos incluso controlar el layout del contenedor padre basándonos en los hijos */
.container:has(.product-card:not(:has(button))) {
  grid-template-columns: 1fr; /* Si hay alguna tarjeta sin botón, que ocupe todo el ancho */
}

.container:has(.product-card:has(button)) {
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

En este ejemplo, la regla .product-card:has(button) aplica un estilo específico si la tarjeta contiene un botón. Pero, lo más interesante es cómo container:has() ajusta el grid-template-columns de todo el contenedor, basándose en si alguna tarjeta en su interior tiene (o no tiene) un botón. ¡Esto es diseño dinámico puro CSS!

2. Validaciones de Formulario Visibles al Usuario

Tradicionalmente, para mostrar mensajes de error cuando un campo está inválido, se requería JavaScript para añadir o quitar clases. Con :has(), puedes reaccionar al estado nativo de los formularios.

<form>
  <div class="form-group">
    <label for="username">Usuario:</label>
    <input type="text" id="username" required>
    <span class="error-message">El usuario es obligatorio.</span>
  </div>

  <div class="form-group">
    <label for="email">Email:</label>
    <input type="email" id="email" required>
    <span class="error-message">Por favor, introduce un email válido.</span>
  </div>

  <div class="form-group">
    <label for="password">Contraseña:</label>
    <input type="password" id="password" minlength="8">
    <span class="error-message">La contraseña debe tener al menos 8 caracteres.</span>
  </div>

  <button type="submit">Enviar</button>
</form>
.form-group .error-message {
  display: none;
  color: red;
  font-size: 0.9em;
  margin-top: 5px;
}

/* Muestra el mensaje de error si el input está inválido y ha sido 'tocado' */
.form-group:has(input:invalid:focus) .error-message,
.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
  display: block;
}

/* Estiliza el grupo completo si el input es inválido */
.form-group:has(input:invalid) label {
  color: red;
}

.form-group:has(input:invalid) input {
  border-color: red;
}

/* Estiliza el grupo completo si el input es válido */
.form-group:has(input:valid) label {
  color: green;
}

.form-group:has(input:valid) input {
  border-color: green;
}

Aquí, estamos usando :has() junto con :invalid, :focus, y :not(:placeholder-shown) para mostrar mensajes de error y estilizar el grupo del formulario basándonos en el estado de validación del input hijo. Esto es increíblemente poderoso y reduce la necesidad de un control pesado con JavaScript para feedback de validación visual.

3. Navegación Interactiva (Mega Menús)

Podemos usar :has() para crear mega menús que se abren cuando un elemento padre es hovered o focused, y sus submenús son hijos.

<nav class="main-nav">
  <ul>
    <li><a href="#">Inicio</a></li>
    <li class="has-submenu">
      <a href="#">Productos</a>
      <div class="submenu">
        <a href="#">Categoría 1</a>
        <a href="#">Categoría 2</a>
        <a href="#">Categoría 3</a>
      </div>
    </li>
    <li><a href="#">Contacto</a></li>
  </ul>
</nav>
.submenu {
  display: none;
  position: absolute;
  background-color: #333;
  padding: 10px;
  min-width: 150px;
  z-index: 100;
}

.main-nav .has-submenu:has(a:hover) .submenu,
.main-nav .has-submenu:has(a:focus) .submenu {
  display: block;
}

.main-nav .has-submenu:hover > a {
  color: #fff; /* Cambiar color del enlace padre al hacer hover */
  background-color: #555;
}

Con :has(a:hover) o :has(a:focus), el submenu se mostrará cuando el enlace padre (<a>) reciba el hover o focus, incluso si el submenu no es un hermano adyacente sino un descendiente más profundo. Esto evita tener que aplicar el hover directamente al li padre, lo que puede ser más flexible.

4. Alternar Temas (Dark/Light Mode) con un Solo Checkbox

Aunque esto suele hacerse con JavaScript o variables CSS, :has() puede simplificar el código HTML y CSS en ciertos escenarios.

<input type="checkbox" id="dark-mode-toggle" hidden>

<div class="page-wrapper">
  <label for="dark-mode-toggle">Toggle Dark Mode</label>
  <header>Contenido del encabezado</header>
  <main>Contenido principal</main>
  <footer>Contenido del pie de página</footer>
</div>
body {
  --bg-color: #f0f0f0;
  --text-color: #333;
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s, color 0.3s;
}

/* Si el checkbox está marcado y es un hermano anterior al page-wrapper */
:has(#dark-mode-toggle:checked) .page-wrapper {
  --bg-color: #333;
  --text-color: #f0f0f0;
}

/* O si el checkbox está DENTRO de un elemento que contiene el page-wrapper */
.page-wrapper:has(#dark-mode-toggle:checked) {
  --bg-color: #333;
  --text-color: #f0f0f0;
}

label[for="dark-mode-toggle"] {
  cursor: pointer;
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 5px;
  display: inline-block;
  margin-bottom: 20px;
}

Aquí, la pseudo-clase :has(#dark-mode-toggle:checked) permite que el elemento page-wrapper (o incluso el body si el input es un hermano adyacente) cambie sus variables CSS cuando el checkbox de dark-mode-toggle está marcado. Esto demuestra la capacidad de :has() para afectar a elementos anteriores o superiores en el DOM que no son sus padres directos, siempre que el selector relativo pueda encontrarlos. En este caso, el body o el page-wrapper 'contienen' la condición del input marcado.

5. Estilizar hermanos en función del estado de un elemento

Uno de los superpoderes de :has() es la capacidad de seleccionar un elemento en función del estado de un hermano adyacente o general. Esto es algo que antes era imposible sin JavaScript.

<div class="item-list">
  <input type="checkbox" id="item-a-check">
  <label for="item-a-check">Item A</label>
  <div class="item-details">
    Detalles del Item A.
  </div>

  <input type="checkbox" id="item-b-check">
  <label for="item-b-check">Item B</label>
  <div class="item-details">
    Detalles del Item B.
  </div>
</div>
.item-details {
  display: none;
  border: 1px solid #ddd;
  padding: 10px;
  margin-top: 5px;
  background-color: #f9f9f9;
}

/* Mostrar detalles del hermano SÓLO SI el checkbox anterior está marcado */
.item-list:has(input[type="checkbox"]:checked + label) .item-details {
  display: block;
}

/* Otra forma: Seleccionar directamente el div.item-details si hay un input:checked y un label entre ellos */
input[type="checkbox"]:checked + label + .item-details {
  display: block;
}

Con :has(input[type="checkbox"]:checked + label), estamos seleccionando el .item-list padre si tiene un input marcado seguido inmediatamente por un label. Luego, podemos estilizar el .item-details que es un descendiente de .item-list. Esto es más potente que simplemente usar + o ~ porque :has() permite que el contenedor reaccione a la relación entre sus hijos. Si el input y el item-details fueran hermanos directos, la segunda regla sería suficiente, pero :has() nos da más flexibilidad para estructuras anidadas.

Potencial Increíble

🤯 Limitaciones y Consideraciones

Aunque :has() es muy potente, hay algunas cosas a tener en cuenta.

Rendimiento

El uso excesivo o incorrecto de selectores complejos con :has() podría, en teoría, tener un impacto en el rendimiento, ya que el navegador necesita 'mirar hacia atrás' o evaluar subárboles. Sin embargo, los motores de navegador modernos están altamente optimizados, y en la mayoría de los casos de uso prácticos, el impacto será insignificante. Es más probable que el rendimiento se vea afectado por animaciones costosas o un DOM excesivamente grande y complejo, que por :has() en sí mismo.

Soporte del Navegador

Actualmente, :has() cuenta con un excelente soporte en la mayoría de los navegadores modernos (Chrome, Firefox, Safari, Edge). Sin embargo, siempre es una buena práctica verificar sitios como Can I use para el soporte más actualizado, especialmente si necesitas soportar navegadores más antiguos o nichos específicos.

⚠️ Advertencia: Para proyectos que requieran soporte total en navegadores legacy, quizás necesites un fallback con JavaScript o polyfills, aunque para el desarrollo web moderno, el soporte es robusto.

Anidamiento de :has()

Sí, puedes anidar :has() dentro de otro :has(). Esto puede llevar a selectores extremadamente potentes (y potencialmente complejos de leer).

Ejemplo: Seleccionar un article que tenga un div que a su vez tenga una imagen con la clase .hero-image.

article:has(div:has(.hero-image)) {
  border: 5px solid gold;
}

Esto selecciona el article si contiene un div que, a su vez, contiene un elemento con la clase .hero-image. ¡Las posibilidades son infinitas!

🛠️ Buenas Prácticas y Consejos

  • Prioriza la legibilidad: Aunque :has() permite selectores complejos, trata de mantenerlos legibles. A veces, añadir una clase con JavaScript es más claro que un selector :has() de 5 niveles de anidamiento.
  • Combina con :not(): La combinación de :has() con :not() es extremadamente poderosa para la exclusión condicional. Ejemplo: .card:not(:has(img)) para estilizar tarjetas sin imagen.
  • Usa con variables CSS: Como vimos en el ejemplo del tema oscuro, :has() puede ser excelente para cambiar variables CSS en un ancestro, lo que a su vez afecta a sus descendientes de manera consistente.
  • Prueba en diferentes navegadores: Aunque el soporte es bueno, siempre prueba tus implementaciones en los navegadores objetivo de tu proyecto.
¿`:has()` es un 'selector padre'? Sí, en efecto, es lo más cercano a un 'selector padre' que hemos tenido nunca en CSS. Permite seleccionar un elemento basándose en sus descendientes, lo que invierte la dirección de selección tradicional de CSS.
¿Puedo usar `:has()` con pseudo-elementos como `::before` o `::after`? `:has()` funciona con selectores de elementos, atributos, clases, IDs y otras pseudo-clases. Sin embargo, no se puede usar para seleccionar pseudo-elementos. Los pseudo-elementos no son parte del árbol del DOM y no pueden ser el objetivo de `:has()`.

Conclusión

La pseudo-clase :has() es un game-changer para el desarrollo web front-end. Permite una lógica de estilo condicional mucho más sofisticada y reduce la dependencia de JavaScript para muchas tareas de manipulación del DOM. Al comprender y aplicar :has() de manera efectiva, podrás crear interfaces más robustas, limpias y eficientes, llevando tus habilidades de HTML y CSS al siguiente nivel.

¡Anímate a experimentar con :has() en tus próximos proyectos y descubre todo su potencial!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!