Articulo publicado el
Aprende a usar el selector de relación :has() para aplicar estilos en función de los hijos, el estado y el contexto, sin recurrir a JavaScript.
Durante años, CSS solo permitía seleccionar hacia abajo y hacia los lados: podías apuntar a los descendientes de un elemento o a sus hermanos posteriores, pero nunca a un elemento padre en función de lo que contiene. Cada vez que necesitábamos "si esta tarjeta tiene una imagen, cambia el layout de la tarjeta" acabábamos añadiendo una clase con JavaScript.
El selector :has() rompe esa limitación. Es un selector de relación que se evalúa sobre el elemento al que se aplica, pero cuya condición depende de lo que hay dentro o después de ese elemento. Coloquialmente se le llama el "selector padre", aunque en realidad es mucho más que eso: permite estilizar un elemento según su contenido, su estado o su contexto.
:has()es una pseudo-clase funcional que representa un elemento si cualquiera de los selectores relativos que se le pasan como argumento coincide con al menos un elemento.
En este artículo veremos la sintaxis real, patrones que resuelven problemas cotidianos, sus implicaciones de rendimiento y accesibilidad, y los casos en los que conviene evitarlo.
La regla básica es sencilla: escribes el elemento objetivo, y dentro de :has() un selector relativo que describe la condición.
/* Estiliza el <figure> SOLO si contiene un <figcaption> */
figure:has(figcaption) {
border: 1px solid var(--border, #e2e2e2);
padding: 0.5rem;
}
La clave está en entender que el elemento estilizado es el figure, no el figcaption. El argumento de :has() es un selector relativo, lo que significa que puedes usar combinadores como >, + o ~ empezando por el elemento objetivo.
/* El artículo tiene un hijo directo <img> */
article:has(> img) {
}
/* El label va seguido inmediatamente de un input inválido */
label:has(+ input:invalid) {
}
/* La sección contiene, en cualquier nivel, un elemento marcado */
section:has(.destacado) {
}
Puedes pasar varios selectores separados por comas, lo que funciona como un OR lógico. Para un AND, encadena varios :has() o combina condiciones dentro del mismo argumento.
/* OR: la tarjeta tiene una imagen O un vídeo */
.card:has(img, video) {
}
/* AND: la tarjeta tiene imagen Y además un badge */
.card:has(img):has(.badge) {
}
Un detalle importante sobre especificidad: :has() no aporta especificidad por sí mismo, pero sí la aportan los selectores de su interior, tomando el más específico del argumento. Esto lo diferencia de :where(), que siempre neutraliza la especificidad.
La teoría se entiende mejor con problemas concretos que antes exigían JavaScript o marcado extra.
Uno de los usos más potentes es resaltar un grupo de formulario cuando su campo es inválido, sin tocar el DOM desde JS.
.form-field:has(input:invalid:not(:placeholder-shown)) {
--field-color: #d92d20;
}
.form-field:has(input:invalid:not(:placeholder-shown)) label {
color: var(--field-color);
}
.form-field:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block;
}
El truco de :not(:placeholder-shown) evita marcar como error un campo que el usuario aún no ha tocado. Todo el estado visual del grupo depende del estado nativo del input, que es la fuente de verdad más fiable.
Piensa en una tarjeta que debe cambiar de disposición cuando incluye imagen:
/* Tarjeta a una columna por defecto */
.card {
display: grid;
gap: 1rem;
}
/* Si la tarjeta tiene imagen, pasa a dos columnas */
.card:has(> img) {
grid-template-columns: 160px 1fr;
}
Antes esto requería una clase card--with-image calculada en el servidor o en el cliente. Ahora el propio contenido dicta el layout.
Un patrón habitual: bloquear el scroll del documento cuando hay un modal o un menú abierto. Con :has() puedes reaccionar al estado de un elemento profundo desde la raíz.
/* Si en cualquier parte del body hay un dialog abierto,
bloquea el scroll del documento */
body:has(dialog[open]) {
overflow: hidden;
}
Esto elimina la típica clase .no-scroll que se añadía y olvidaba quitar, una fuente clásica de bugs.
CSS nunca tuvo un "combinador de hermano anterior". Con :has() lo emulas de forma natural:
/* Marca el label cuyo checkbox asociado está activado */
label:has(+ input[type="checkbox"]:checked) {
font-weight: 600;
}
:has() es tentador, pero no todo problema es un clavo para este martillo.
Conviene usarlo cuando:
:checked, :invalid, [open]) sin escribir JavaScript.Conviene evitarlo cuando:
*:has(...) sobre todo el documento), porque complica el mantenimiento.:has() está ampliamente soportado en los navegadores modernos (Chrome, Edge, Safari y Firefox lo implementan desde 2023). Aun así, trátalo como una mejora progresiva: la interfaz debe seguir siendo usable sin él. Puedes detectar el soporte con @supports selector().
@supports selector(:has(*)) {
.card:has(> img) {
grid-template-columns: 160px 1fr;
}
}
Puedes consultar el estado de soporte actualizado en MDN.
Sobre rendimiento, los motores modernos optimizan :has() razonablemente bien, pero conviene ser específico: prefiere .card:has(> img) frente a un :has() sin acotar que obligue al navegador a evaluar árboles grandes. Sobre accesibilidad, recuerda que :has() solo cambia estilos: no altera el árbol de accesibilidad ni sustituye a los atributos ARIA. Si un cambio visual comunica información (por ejemplo, un estado de error), asegúrate de que también existe a nivel semántico con aria-invalid, role o mensajes asociados.
El selector :has() traslada a CSS decisiones que antes vivían obligatoriamente en JavaScript: estilizar en función del contenido, del estado nativo del DOM o del contexto. El resultado es un CSS más declarativo, con menos clases sincronizadas a mano y menos puntos de fallo. Empléalo con criterio, mantén los selectores específicos y consérvalo como una capa de mejora progresiva, y ganarás interfaces más robustas con bastante menos código.
Estos articulos pueden interesarte si te gusto este articulo.