<!-- Canonical: https://lucioduran.com/es/blog/css-has-selector-quantity-queries-patterns
Language: es
Content-SHA256: 7c20c0b47ec8b30dc3e61be4fd75eaf7b2df3dd53d6cb7d875b582f291227923
-->

Frontend

# CSS :has() Selector: Quantity Queries, Selección de Padres y Patrones Previamente Imposibles

2025-11-107 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns>)

## Selección de Padres: El Caso de Uso Obvio

El ejemplo canónico, y todavía el caso de uso más común:

```css
/* Estilar el contenedor card cuando contiene una imagen */
.card:has(> img) {
 grid-template-rows: 200px 1fr;
}

.card:not(:has(> img)) {
 grid-template-rows: 1fr;
}
```

Antes de ` :has() `, esto requería o dos variantes de card en tu biblioteca de componentes (` .card--con-imagen `, ` .card--solo-texto `) o JavaScript que chequeara la imagen y agregara una clase. Ambos son cargas de mantenimiento. La solución CSS reacciona al contenido real.

Un ejemplo más práctico, un formulario donde el contenedor del botón submit cambia layout según si existen errores de validación:

```css
.form-actions:has(~ .error-message) {
 flex-direction: column;
 gap: 0.5rem;
}

.form-actions:has(~ .error-message)::before {
 content: "Por favor corregí los errores de arriba";
 color: var(--color-error);
 font-size: 0.875rem;
}
```

El combinador de hermano general ` ~ ` dentro de ` :has() ` es lo que hace que funcione, el elemento form actions chequea si algún hermano siguiente es un mensaje de error. Sin JavaScript. Sin state management. CSS puro respondiendo a la estructura del DOM.

## Quantity Queries: Implementación Nativa en CSS

Las quantity queries te dejan estilar un contenedor diferente según cuántos hijos tiene. Esto era un problema resuelto en Sass con mixins que generaban combinaciones de ` :nth-child ` + ` :nth-last-child `, pero ` :has() ` lo hace nativo y legible.

```css
/* Un solo item, ancho completo */
.grid:has(> :nth-child(1):last-child) {
 grid-template-columns: 1fr;
}

/* 2 items, dos columnas */
.grid:has(> :nth-child(2):last-child) {
 grid-template-columns: repeat(2, 1fr);
}

/* 3 items, tres columnas */
.grid:has(> :nth-child(3):last-child) {
 grid-template-columns: repeat(3, 1fr);
}

/* 4+ items, grid auto-fill */
.grid:has(> :nth-child(4)) {
 grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
```

El truco es el patrón ` :nth-child(n):last-child `. ` :nth-child(3):last-child ` coincide con un elemento que es tanto el tercer hijo COMO el último hijo, o sea que el contenedor tiene exactamente 3 hijos.

Este patrón funciona bien en dashboards donde la cantidad de cards de KPI varía por rol de usuario. Antes, los componentes React calculaban una prop ` columns ` y la pasaban como variable CSS. Ahora el CSS lo maneja autónomamente:

```css
.kpi-dashboard:has(> .kpi-card:nth-child(5)) {
 /* 5+ cards: switchear a layout compacto */
 --kpi-gap: 0.75rem;
 --kpi-font-size: 0.875rem;
}

.kpi-dashboard:has(> .kpi-card:nth-child(5)) .kpi-card {
 padding: 0.75rem;
}
```

### Queries de Rango

También es posible apuntar a rangos, "entre 3 y 6 items":

```css
/* Tiene al menos 3 hijos Y el 7mo hijo NO existe */
.list:has(> :nth-child(3)):not(:has(> :nth-child(7))) {
 /* 3-6 items */
 columns: 2;
}
```

Esto se lee como: "un ` .list ` que tiene un 3er hijo (así que al menos 3) pero no tiene un 7mo hijo (así que como mucho 6)." Es verboso, pero funciona sin preprocesador.

## Estilos Conscientes de Hermanos

` :has() ` combinado con combinadores de hermanos te deja estilar elementos basándote en lo que viene después de ellos. Esto era completamente imposible antes.

```css
/* Estilar un heading distinto cuando le sigue un subtítulo */
h2:has(+ .subtitle) {
 margin-bottom: 0.25rem;
}

h2:not(:has(+ .subtitle)) {
 margin-bottom: 1.5rem;
}

/* Estilar el último item antes de un divisor */
.nav-item:has(+ .nav-divider) {
 margin-bottom: 0;
 padding-bottom: 0.5rem;
 border-bottom: 1px solid var(--border);
}
```

Este patrón resulta valioso en rediseños de navegación. El approach viejo requiere clases ` .nav-item--antes-de-divisor ` generadas por el template engine. La versión con ` :has() ` puede eliminar decenas de líneas de lógica de template.

## Cascada de Estados de Formularios

Este es el punto donde ` :has() ` empieza a sentirse como si estuviera reemplazando JavaScript entero. Es posible cascadear estados de elementos de formulario hacia arriba a cualquier ancestro:

```css
/* Toda la sección del form recibe feedback visual basado en validez */
.form-section:has(:invalid) {
 border-left: 3px solid var(--color-error);
}

.form-section:has(:focus-within) {
 background: var(--bg-active);
}

/* Label del fieldset cambia cuando algún input hijo tiene contenido */
.field-group:has(input:not(:placeholder-shown)) label {
 transform: translateY(-1.5rem) scale(0.85);
 color: var(--color-primary);
}

/* Deshabilitar submit cuando el form tiene campos inválidos */
form:has(:invalid) button[type="submit"] {
 opacity: 0.5;
 pointer-events: none;
}
```

Ese último, deshabilitar submit basado en validez del form, antes requería un hook ` useEffect ` mirando cada campo del formulario. Ahora es una regla CSS.

### El Hack del Checkbox, Evolucionado

El clásico hack del checkbox (` :checked ` + combinador de hermano ` ~ `) estaba limitado a estilar hermanos. Con ` :has() `, es posible estilar cualquier cosa en el documento basándote en el estado del checkbox:

```css
/* Toggle de dark mode, cero JavaScript */
html:has(#theme-toggle:checked) {
 --bg: #1a1816;
 --text: #e8e6e3;
 --accent: #d97757;
 color-scheme: dark;
}

/* Colapso de sidebar */
.layout:has(#sidebar-toggle:checked) .sidebar {
 transform: translateX(-100%);
}

.layout:has(#sidebar-toggle:checked) .main-content {
 grid-column: 1 / -1;
}
```

Esto no es un argumento para reemplazar toda la interactividad JavaScript con hacks de checkbox. Pero para toggles binarios que afectan estado puramente visual, este approach es más limpio que un React context provider.

## Patrones Responsivos Habilitados por :has()

Un patrón notable, layout responsivo que se adapta al contenido, no al viewport:

```css
/* Contenedor con sidebar opcional */
.page-layout {
 display: grid;
 grid-template-columns: 1fr;
}

.page-layout:has(> .sidebar) {
 grid-template-columns: 1fr 300px;
}

/* Pero colapsar a columna única si el sidebar está vacío */
.page-layout:has(> .sidebar:empty) {
 grid-template-columns: 1fr;
}

/* Y switchear basado en densidad de contenido */
.page-layout:has(> .sidebar > :nth-child(5)) {
 grid-template-columns: 1fr 400px;
 /* Sidebar más ancho cuando tiene 5+ items */
}
```

Esto responde al contenido real del sidebar, no a una prop, no a una clase, no a un media query. El layout emerge de la estructura del contenido.

Otro más, estilos condicionales de tabla basados en cantidad de columnas:

```css
/* Tabla compacta cuando tiene muchas columnas */
table:has(th:nth-child(8)) {
 font-size: 0.75rem;
}

table:has(th:nth-child(8)) th,
table:has(th:nth-child(8)) td {
 padding: 0.25rem 0.5rem;
}

/* Highlight de fila en hover, pero solo si la tabla tiene columna interactiva */
table:has(th.actions) tbody tr:hover {
 background: var(--row-hover);
 cursor: pointer;
}
```

## Consideraciones de Performance

Ahora la parte que importa a escala. ` :has() ` es una pseudo-clase relacional, el browser tiene que evaluar el selector argumento para cada elemento que matchee el sujeto. Esto tiene costos reales.

Al profilar una página con 2.000 nodos DOM y varios selectores ` :has() `, se obtienen estos resultados de Chrome DevTools Selector Stats:

```
.card:has(> img) → 0.3ms (rápido, hijo directo)
.card:has(img) → 1.8ms (lento, búsqueda descendiente)
.grid:has(:nth-child(4):last-child) → 0.4ms (rápido, estructural)
.page:has(.error) → 3.2ms (lento, descendiente profundo)
.a:has(.b:has(.c)) → 8.7ms (evitar esto)
```

Las reglas de oro:

1. **Utilizar combinadores de hijo directo** (` > `) siempre que puedas. La diferencia entre ` :has(> img) ` y ` :has(img) ` es aproximadamente 6x en benchmarks.
2. **Evitar ` :has() ` en subárboles que se actualizan frecuentemente.** Si un nodo DOM cambia seguido (listas virtuales, feeds en tiempo real), cada mutación dispara re-evaluación de ` :has() ` para todos los ancestros.
3. **No combines ` :has() ` con ` * `.** El selector ` .parent:has(* > .child) ` es patológico, recorre todo el subárbol en cada nivel.
4. **Profileá antes de optimizar.** En la mayoría de aplicaciones reales, el matching de selectores CSS es un error de redondeo comparado con layout y paint. No evites ` :has() ` por miedo infundado.

```css
/* BIEN, scope restringido */
.card:has(> .badge) { /* ... */ }

/* MAL, búsqueda descendiente sin restricción */
.page:has(.badge) { /* ... */ }

/* TERRIBLE, :has() anidado con búsqueda descendiente */
.page:has(.section:has(.card:has(.badge))) { /* ... */ }
```

## Estrategia de Mejora Progresiva

Para el caso raro donde se necesita soportar browsers sin ` :has() `:

```css
/* Estilos base, funcionan en todos lados */
.card {
 grid-template-rows: 1fr;
}

/* Estilos mejorados, solo en browsers con :has() */
@supports selector(:has(*)) {
 .card:has(> img) {
 grid-template-rows: 200px 1fr;
 }
}
```

El query ` @supports selector() ` ya tiene buen soporte a esta altura, haciendo este patrón seguro.

## Para Cerrar

` :has() ` es la feature de CSS más significativa desde Flexbox. No es hipérbole, cambia fundamentalmente la relación entre CSS y la estructura del DOM. Patrones que requerían JavaScript (selección de padres), preprocesadores (quantity queries), o que eran simplemente imposibles (awareness de hermanos hacia adelante) ahora son CSS nativo.

El uso de ` :has() ` en producción desde mediados de 2024 ha demostrado beneficios medibles. Los codebases que lo adoptan pueden reducir bundles JavaScript en 10-15KB eliminando código de DOM-querying que ahora vive en CSS. Los selectores son legibles, la performance es aceptable, y la carga de mantenimiento es dramáticamente menor.

Escribir el CSS que describe la relación entre tus elementos. Dejar que el browser se encargue del resto.

## Preguntas frecuentes

### ¿:has() causa problemas de performance en producción?

Depende del uso. Selectores :has() simples con combinadores de hijo directo performan comparablemente a selectores regulares. Los patrones caros son :has() profundamente anidados, :has() combinado con selectores universales, y :has() en árboles DOM que se actualizan frecuentemente. Profileá con Selector Stats de Chrome DevTools si te preocupa.

### ¿Puede :has() reemplazar completamente JavaScript para estilos interactivos?

Para muchos casos, sí. Hacks con checkbox usando :has(:checked), validación de formularios con :has(:invalid), y cambio de layout basado en presencia de contenido funcionan sin JS. Pero :has() no puede responder a eventos que no cambian estado del DOM (timing de hover, posición de scroll, carga async de datos), así que JavaScript sigue siendo dueño de esos.

### ¿Cómo está el soporte de browsers para :has() en 2026?

Baseline desde diciembre 2023. Chrome 105+, Safari 15.4+, Firefox 121+. La única brecha son versiones viejas de Samsung Internet y UC Browser, que son cada vez más irrelevantes para la mayoría de audiencias. Mandalo.

## Tips

1. **Tip:** Nunca anidés selectores ` :has() ` más de 2 niveles de profundidad. El browser tiene que evaluar el selector interior para cada elemento que matchee el exterior, ` .grid:has(.card:has(.badge)) ` es un disparo en el pie de performance en DOMs grandes.
2. **Tip:** Utilizar ` :has(> .hijo-directo) ` con el combinador de hijo directo siempre que puedas. Restringe el scope de búsqueda y evita que el motor recorra todo el subárbol.
3. **Tip:** Las quantity queries con ` :has() ` son poderosas pero frágiles, siempre acompañalas con un selector de contenedor. ` :has(:nth-child(4)) ` solo va a coincide conr *cualquier* ancestro de un cuarto hijo, no solo el padre inmediato.
4. **Tip:** Para toggles de dark mode, ` html:has(#dark-mode:checked) ` te deja estilar toda la página desde el estado de un checkbox sin una sola línea de JavaScript. Esto puede reemplazar un setup de React context + localStorage con 3 líneas de CSS.

- css
- has-selector
- quantity-queries
- responsive-design
- frontend
- performance
- selectores
- css-architecture

[Volver al Blog](<https://lucioduran.com/es/blog>)
