El selector :has() de CSS: una guía práctica
Cómo :has() le da por fin a CSS un selector de padre, con sus patrones, trampas y costes de rendimiento.

Durante dos décadas, el chiste recurrente en CSS fue que no existía un selector de padre. Podías estilizar un hijo según su padre, pero nunca al revés — "estiliza este .card si contiene una img" requería JavaScript, alternar una clase, o algún workaround que en realidad no funcionaba. La pseudoclase :has(), ya disponible en todos los navegadores evergreen, por fin cierra esa brecha. No es un selector de padre en sentido literal — es una pseudoclase relacional — pero en la práctica hace exactamente lo que los desarrolladores han querido desde CSS2: dejar que la estructura del DOM determine las decisiones de estilo sin tocar JavaScript.
La trampa es que :has() es lo bastante potente como para usarse mal. Parece una simple comprobación de "¿esto contiene aquello?", pero en realidad es un selector completo evaluado de forma relacional, y eso tiene implicaciones reales tanto para tu arquitectura CSS como para el rendimiento de renderizado.
Qué hace realmente :has()
:has() hace match con un elemento si al menos uno de los selectores que recibe coincide con algo relativo a ese elemento — un descendiente, un hermano o un hijo directo. La documentación de MDN sobre :has() lo llama "pseudoclase relacional" precisamente por eso: no selecciona lo que está dentro de los paréntesis, sino que selecciona al sujeto en función de lo que hay dentro.
/* Style a card differently if it contains an image */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
/* Style a form group when its input is invalid */
.form-group:has(input:invalid) {
border-left: 3px solid var(--color-danger);
}
/* Style a label based on a sibling checkbox's state */
.toggle-row:has(input[type="checkbox"]:checked) {
background: var(--color-surface-active);
}Fíjate en el tercer ejemplo: :has() no se limita a descendientes. Combinado con el combinador general de hermanos ~, te permite reaccionar al estado de un elemento hermano — algo que antes era imposible sin reordenar el markup para explotar el famoso "checkbox hack".
Patrones del mundo real
El patrón que más aparece en código de producción es el layout condicional basado en la cantidad de contenido. Antes de :has(), esto significaba un ResizeObserver de JavaScript o una clase renderizada en el servidor según la longitud de un array.
/* ❌ Bad: JavaScript just to add a layout class */
const grid = document.querySelector('.card-grid');
if (grid.children.length > 4) {
grid.classList.add('dense-layout');
}/* ✅ Good: pure CSS, no observer, no re-render */
.card-grid:has(> .card:nth-child(5)) {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}El segundo patrón es el estilado consciente de validación sin que una librería de validación toque las clases de tu DOM:
/* Highlight the whole fieldset if any required field is empty */
fieldset:has(input:required:placeholder-shown) {
outline: 2px dashed var(--color-warning);
}
/* Disable a submit button declaratively when a form is invalid */
button[type="submit"]:has(+ .form-errors:not(:empty)) {
pointer-events: none;
opacity: 0.5;
}Un tercer caso de uso habitual es el theming delimitado por contenido, donde un componente necesita saber si tiene un hijo específico sin depender de una clase en build-time o runtime:
/* Give articles with a hero image more generous top spacing */
article:has(> .hero-image) {
padding-block-start: 0;
}
article:not(:has(> .hero-image)) {
padding-block-start: 2rem;
}Ese último par — :has() combinado con :not() — es donde la pseudoclase empieza a reemplazar categorías enteras de lógica de renderizado condicional que antes vivía en el código de tus componentes.
Consideraciones de rendimiento
:has() no es gratis. Como puede hacer match basándose en descendientes, una implementación ingenua obligaría al navegador a recorrer todo el subárbol de cada elemento candidato en cada recálculo de estilos. Los fabricantes de navegadores mitigaron esto con optimizaciones internas — rastreando qué elementos son "has-anchors" e invalidando estilos de forma incremental en lugar de reescanear todo el DOM — pero el modelo de coste sigue siendo distinto al de un simple selector de descendientes.
Algunas reglas prácticas que importan en código real:
- Acota el lado derecho de
:has()tanto como puedas..card:has(img)es barato..card:has(* img)o selectores profundamente anidados dentro de:has()cuestan más porque el navegador tiene que revisar más subárbol. - Evita usar
:has()en elementos que se re-renderizan constantemente, como los ítems de una lista en un contenedor de scroll virtualizado, donde cada mutación puede disparar una nueva comprobación relacional. - Prefiere
:has()a unMutationObserversolo cuando la comprobación sea simple. Si necesitas reaccionar a cambios de estado complejos (respuestas de API, valores calculados), las clases controladas por JavaScript siguen siendo la herramienta adecuada —:has()resuelve condiciones estructurales y de estado, no lógica de negocio.
/* ❌ Expensive: unbounded descendant search */
.sidebar:has(.nav-item.active .submenu a:hover) {
box-shadow: inset 4px 0 0 var(--color-accent);
}
/* ✅ Cheap: shallow, direct relationship */
.sidebar:has(> .nav-item.active) {
box-shadow: inset 4px 0 0 var(--color-accent);
}Si estás auditando una hoja de estilos existente por el rendimiento de :has(), revisa el tiempo de "Style recalculation" en el panel de Performance de DevTools antes y después de añadir un selector relacional — el coste suele ser insignificante hasta que el subárbol se vuelve profundo o la cantidad de elementos es grande.
Soporte de navegadores y mejora progresiva
:has() está definido en CSS Selectors Level 4, y a fecha de esta publicación cuenta con soporte en todos los navegadores evergreen principales — navegadores basados en Chromium, Safari y Firefox. Dicho esto, "evergreen" no significa "el navegador de todos tus usuarios", especialmente si das soporte a entornos corporativos con instalaciones antiguas.
El patrón seguro es la detección de funcionalidades con @supports, que funciona porque :has() se diseñó para ser detectable:
@supports selector(:has(a)) {
.card:has(img) {
grid-template-columns: 120px 1fr;
}
}
@supports not selector(:has(a)) {
/* Fallback: always show the two-column layout */
.card {
grid-template-columns: 120px 1fr;
}
}Esto te da un layout que se degrada con elegancia en lugar de fallar en silencio. Si tu fallback requiere algo más que un ajuste de layout — por ejemplo, deshabilitar un botón de envío — mantén la comprobación basada en JavaScript como red de seguridad, en lugar de depender solo de CSS para algo que afecta a la corrección de un formulario.
Combinando :has() con otros selectores
El verdadero poder aparece cuando :has() se combina con otras funcionalidades de Selectors Level 4, en particular :not() y las container queries. Un patrón al que recurro a menudo: estilizar un layout según tanto su contenido como su espacio disponible.
.content-area {
container-type: inline-size;
}
/* Only widen the sidebar gap if the container is roomy
AND the sidebar actually has nested navigation */
@container (min-width: 640px) {
.layout:has(.sidebar .submenu) {
grid-template-columns: 280px 1fr;
}
}Este tipo de composición — conciencia estructural (:has()) más conciencia espacial (container queries) — es lo que permite que el CSS de un componente sea genuinamente autocontenido, sin que un componente padre tenga que pasar props de layout solo para acomodar la presencia de un hijo.
Conclusiones clave
:has()es una pseudoclase relacional, no un selector de padre literal — hace match con el sujeto según los descendientes, hijos o hermanos que especifiques dentro de los paréntesis.- Úsalo para reemplazar JavaScript que solo existe para alternar una clase según la estructura del DOM o el estado de un formulario — ese es su caso de uso más sólido.
- Mantén el selector dentro de
:has()lo más superficial posible; las comprobaciones de descendientes sin límite conllevan un coste real de recálculo de estilos. - Envuelve los usos no críticos en
@supports selector(:has(a))y mantén fallbacks de JavaScript para todo lo que afecte a la corrección de formularios o a la accesibilidad. - Combina
:has()con:not()y container queries para construir componentes que reaccionen tanto a su contenido como a su espacio disponible, sin prop drilling. - Audita cualquier adición de
:has()a un subárbol que mute con frecuencia (listas virtualizadas, feeds en vivo) con el panel de Performance de DevTools antes de publicarlo.


