Durante casi tres décadas, CSS fue capaz de seleccionar un elemento según sus ancestros, sus hermanos o sus propios atributos, pero nunca según su contenido. Dar un estilo distinto a una <section> según contuviera o no una imagen exigía, hasta ahora, añadir una clase mediante JavaScript. El pseudoselector :has() cubre ese vacío: disponible de forma nativa en los navegadores principales desde diciembre de 2023, permite seleccionar un elemento padre o un elemento anterior según lo que le sigue.
La sintaxis básica
:has() recibe como argumento una lista de selectores relativos y se aplica al elemento si al menos uno de esos selectores encuentra una coincidencia por debajo o al lado. Puede verse como el equivalente CSS de una aserción «lookahead» en expresiones regulares: se selecciona un elemento según lo que lo acompaña, sin seleccionar nunca directamente ese elemento que lo acompaña.
Seleccionar un elemento según lo que le sigue
Combinado con un combinador de hermanos, :has() permite reaccionar a un elemento siguiente, algo que CSS no sabía hacer hasta ahora:
/* Reduce el margen de un h1 seguido inmediatamente de un h2 */
h1:has(+ h2) {
margin-bottom: 0.25rem;
}
/* Combinado con :is() para varios niveles de títulos */
:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
margin-bottom: 0.25rem;
}Es uno de los casos de uso más citados: ajustar automáticamente el espacio entre un título y el subtítulo que lo sigue directamente, sin tener que añadir una clase de utilidad en el marcado en cada aparición.
Formularios y componentes de interfaz
El caso de uso más extendido en producción son los formularios y los componentes interactivos, donde JavaScript servía hasta ahora únicamente para alternar una clase de estado. Combinado con las container queries, que ya adaptan un componente al tamaño de su contenedor, :has() va un paso más allá adaptándolo también a su contenido:
/* Resalta un campo de formulario inválido */
.form-field:has(input:invalid) {
border-color: var(--color-danger);
}
/* Cambia la apariencia de una tarjeta de producto si se marca una casilla */
.product-card:has(input[type="checkbox"]:checked) {
outline: 2px solid var(--accent);
}
/* Lógica Y: ambas condiciones deben cumplirse */
body:has(video):has(audio) {
--has-rich-media: 1;
}Detectar un estado vacío sin JavaScript
Otro uso muy frecuente en producción consiste en mostrar un mensaje cuando una lista no contiene ningún elemento, una tarea que exigía hasta ahora contar los hijos en el servidor o en JavaScript. Combinado con :not(), :has() permite expresar directamente esa ausencia:
/* Muestra un mensaje por defecto si .list no contiene ningún .item */
.list:not(:has(.item)) .empty-state {
display: block;
}
.list:has(.item) .empty-state {
display: none;
}El mismo principio se aplica a un carrito de compra vacío, una bandeja de entrada sin mensajes o un panel sin ningún widget configurado: casos en los que la condición «este elemento no contiene nada» era antes una comprobación en JavaScript ejecutada tras cada actualización del DOM, y que ahora puede seguir siendo declarativa y actualizarse automáticamente con el contenido.
Compatibilidad con navegadores
:has() forma parte de las funcionalidades «Baseline», es decir, disponibles sin prefijo ni activación en todos los navegadores principales desde diciembre de 2023. La compatibilidad cubre tanto las versiones de escritorio como las móviles de:
| Navegador | Versión mínima |
|---|---|
| Chrome / Edge | 105 y posteriores |
| Safari (escritorio e iOS) | 15.4 y posteriores |
| Firefox | 121 y posteriores |
| Opera | 91 y posteriores |
Para un proyecto que todavía deba dar soporte a versiones antiguas de estos navegadores, una prueba de compatibilidad mediante @supports selector(:has(a)) permite ofrecer una versión degradada limpia en lugar de dejar que falle en silencio toda una hoja de estilos.
Especificidad y límites que conviene conocer
:has() toma la especificidad del selector más específico que recibe como argumento, igual que :is() y :not(). Conviene tener presentes dos límites antes de adoptarlo de forma generalizada: no puede anidarse dentro de otro :has(), y los pseudoelementos no son selectores válidos dentro de sus paréntesis, lo que evita bucles de dependencia condicional.
La trampa de compatibilidad más frecuente: si el navegador no admite :has(), falla en silencio todo el bloque del selector, no solo la parte de :has(). Para una degradación más segura en código que aún deba funcionar en navegadores antiguos, envolverlo en :is() o :where() limita los efectos colaterales, y una prueba de soporte mediante @supports selector(:has(a)) sigue siendo el método más explícito.
La trampa de rendimiento que hay que evitar
Como :has() obliga al motor de renderizado a examinar los descendientes o los hermanos de un elemento antes de decidir si coincide, un anclaje demasiado amplio puede desencadenar recorridos costosos de subárboles en páginas grandes.
:has() traslada a CSS una lógica que antes obligaba a escuchar eventos en JavaScript solo para alternar una clase de estado.
Cuándo sigue siendo necesario JavaScript
:has() no lo sustituye todo. Las interacciones que dependen de un estado de la aplicación, de datos asíncronos o de lógica de negocio siguen siendo cosa de JavaScript; del mismo modo, las decisiones de tema claro u oscuro siguen apoyándose en los design tokens en lugar de en la selección de contenido. En cambio, todo lo que consistía en añadir o quitar una clase solo para reflejar el estado interno del DOM —un campo relleno, una casilla marcada, un hijo presente o ausente— se convierte en un candidato natural para :has(), con menos JavaScript que mantener y menos riesgo de que la clase se desincronice del estado real del DOM.
Lo que hay que recordar
:has() está disponible de forma nativa en todas las versiones recientes de los navegadores principales desde diciembre de 2023 y ya puede sustituir buena parte del JavaScript de utilidad dedicado a gestionar clases de estado. Su uso exige, no obstante, la misma disciplina que cualquier selector combinado: anclar sobre un contenedor preciso, restringir la parte interna con combinadores directos y reservar :has() para los casos en que la relación entre el elemento y su contenido sea realmente estructural.
Es el tipo de funcionalidad CSS que no hace ruido pero que cambia en silencio buena parte de los hábitos de componentes en cuanto se empieza a usar. En mis últimos proyectos he sustituido varios fragmentos de JavaScript que solo servían para alternar una clase de estado por un simple :has(), con una ganancia clara en legibilidad. El único riesgo real es abusar de selectores demasiado amplios por comodidad: funciona muy bien en una página ligera durante el desarrollo, y se nota mucho menos cuando la página ya está cargada de contenido real — Simon Janvier.
Para saber más: la documentación de MDN sobre el selector :has().
