El problema del selector de padre
Durante dos décadas, CSS solo permitió seleccionar elementos descendientes. Si queríamos aplicar un estilo al padre en función de lo que contenía, necesitábamos JavaScript o truculencias con clases auxiliares. Una tarjeta que cambia de color cuando incluye una imagen, un formulario que resalta un grupo completo cuando contiene un campo inválido, un artículo que muestra un icono si tiene enlaces externos: todos estos casos requerían lógica externa al CSS.
El selector :has() resuelve ese vacío histórico. Aplica estilos a un elemento solo si cumple la condición descrita entre paréntesis, donde esa condición se refiere a sus descendientes u otros elementos relacionados. Se le conoce informalmente como selector de padre, aunque su alcance va más allá.
La sintaxis básica es directa:
elemento:has(selector) {
/* estilos aplicados al elemento si contiene "selector" */
}
Veamos un caso simple: una tarjeta que añade borde especial cuando tiene una imagen en su interior.
.tarjeta:has(img) {
border: 2px solid #3b82f6;
padding: 0;
}
.tarjeta:has(img) .contenido {
padding: 1rem;
}
Con esta regla, cualquier .tarjeta que contenga al menos una etiqueta <img> recibirá el borde azul. Las tarjetas sin imagen mantienen el estilo por defecto. El CSS describe la condición directamente, sin necesidad de marcar la tarjeta con una clase añadida por JavaScript.
Combinación con pseudoclases de estado
La utilidad real de :has() emerge cuando lo combinamos con pseudoclases de estado como :checked, :focus, :invalid, :disabled o :empty. Esta unión permite crear componentes que reaccionan al estado de sus partes internas con puro CSS.
Un formulario que resalta el grupo completo cuando un campo es inválido:
.grupo-campo:has(input:invalid) {
background: #fef2f2;
border-left: 4px solid #dc2626;
}
.grupo-campo:has(input:invalid) label {
color: #dc2626;
font-weight: 600;
}
El siguiente ejemplo estiliza una lista de opciones según si el usuario ha marcado algún checkbox:
.lista-opciones:has(input:checked) {
background: #eff6ff;
}
.lista-opciones:not(:has(input:checked)) {
opacity: 0.8;
}
La combinación de :has() con :not() amplía el poder expresivo. Podemos seleccionar lo contrario: elementos que no contienen cierto patrón. Un párrafo que muestra un icono solo si no tiene enlaces:
p:not(:has(a))::before {
content: "\1F4DD ";
}
Selección de hermanos con :has()
El selector :has() también admite combinadores como + (hermano adyacente) y ~ (hermano general). Esto permite referirse a elementos relacionados que no son hijos, ampliando todavía más los casos de uso.
Un ejemplo frecuente es ajustar el margen de un encabezado en función de lo que viene después. Si tras un h2 viene un párrafo, reducimos el espaciado inferior; si viene otro encabezado, lo aumentamos.
h2:has(+ p) {
margin-bottom: 0.5rem;
}
h2:has(+ h3) {
margin-bottom: 1.5rem;
}
Otro escenario habitual: cambiar la apariencia de una tabla solo cuando tiene encabezado.
table:has(thead) {
border-top: 3px solid #1e40af;
}
El selector
:has()puede contener selectores complejos y combinadores. Todo lo que escribas dentro de los paréntesis se evalúa como una consulta sobre los descendientes o hermanos del elemento principal.
Componentes interactivos sin JavaScript
Muchos patrones de interfaz que antes requerían JavaScript ahora se resuelven con :has() y un poco de HTML semántico. El clásico menú desplegable que se abre al hacer clic, la navegación que cambia cuando hay un elemento activo, o un acordeón con animación, pueden construirse combinando :has() con elementos nativos como <details> o checkboxes invisibles.
Un acordeón que resalta el contenedor completo al desplegarse:
.item-acordeon:has(details[open]) {
background: #f9fafb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.item-acordeon:has(details[open]) summary {
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
}
Un menú lateral que resalta la sección activa basándose en un enlace con clase activo:
.menu-lateral li:has(a.activo) {
background: #eff6ff;
border-right: 3px solid #3b82f6;
}
La estrategia consiste en dejar que el HTML represente el estado mediante clases, atributos o elementos nativos, y que el CSS responda visualmente a ese estado.
flowchart LR
A[Usuario interactua] --> B[HTML cambia de estado]
B --> C{CSS con :has detecta el cambio}
C --> D[Estilos aplicados al padre]
C --> E[Estilos aplicados a hermanos]
Especificidad y consideraciones
El selector :has() tiene una especificidad particular: toma la mayor de las especificidades de los selectores que contiene. Por ejemplo, div:has(.destacado) pesa como si tuviera una clase, mientras que div:has(#principal) pesa como si tuviera un id.
Esto es importante porque los selectores con :has() pueden volverse muy específicos y afectar a la cascada:
/* Este selector pesa como clase por la presencia de .error */
form:has(.error) {
border: 1px solid #dc2626;
}
/* Este pesa como id por la presencia de #principal */
section:has(#principal) {
padding: 2rem;
}
En cuanto a rendimiento, los navegadores modernos están optimizados para evaluar :has() de forma eficiente. Sin embargo, conviene no abusar de selectores muy generales como *:has(...) que obligan al motor a comprobar cada elemento del documento. Es preferible restringir el ámbito con clases o etiquetas específicas.
Anidamiento de :has()
El selector :has() puede anidar otros selectores en su interior y combinarse con anidamiento nativo CSS. Esto resulta útil cuando un componente tiene varias variantes según su contenido:
.tarjeta-producto {
padding: 1rem;
border: 1px solid #e5e7eb;
&:has(.etiqueta-oferta) {
border-color: #f59e0b;
background: #fffbeb;
}
&:has(.agotado) {
opacity: 0.6;
pointer-events: none;
}
&:has(img.premium) {
border-width: 2px;
border-color: #8b5cf6;
}
}
El bloque describe un componente con tres variantes controladas por la presencia de distintos hijos. Todo el comportamiento visual queda recogido en un único sitio, con cero JavaScript involucrado.
El selector :has() es uno de los avances más significativos de CSS en años. Cambia el paradigma de cómo pensamos los componentes: ya no necesitamos describir solo el punto final del estilo, podemos describir las condiciones bajo las que aparece. En combinación con otras características modernas como anidamiento, container queries o :is() y :where(), forma la base del CSS declarativo actual.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en CSS
Documentación oficial de CSS
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, CSS es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de CSS
Explora más contenido relacionado con CSS y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Usar el selector :has() para aplicar estilos a un elemento padre según sus descendientes, combinar :has() con pseudoclases de estado y diseñar componentes dinámicos sin JavaScript.