🎨 Frontend

CSS moderno: container queries, :has() y las funciones que cambian cómo escribimos estilos

Container queries, :has() y un puñado de funciones nuevas llevan un par de años con soporte completo en navegadores, pero la mayoría de proyectos los sigue evitando por costumbre. Qué patrones de JavaScript y de librerías CSS dejan de tener sentido gracias a esto.

📅 9 de julio de 2026 ⏱️ 9 min de lectura ✍️ Equipo ProgramacionWebs

Durante años, buena parte de lo que hoy resuelve el propio navegador se resolvía con JavaScript: un ResizeObserver para saber si una tarjeta cabía en dos columnas o en una, una librería para calcular si un formulario tenía errores y aplicar un estilo al contenedor, un preprocesador solo para poder anidar selectores sin repetir el padre en cada línea. Nada de eso estaba mal en su momento porque el CSS de entonces no tenía forma nativa de resolverlo. En 2026 sí la tiene, y el problema ya no es técnico: es que la mayoría de bases de código siguen escritas como si estas herramientas no existieran.

Container queries: el problema real que resuelven

Una media query responde a una pregunta: ¿cómo de grande es la ventana del navegador? Es una pregunta útil, pero incompleta para cualquier componente que se reutiliza en contextos distintos. Una tarjeta de producto puede vivir en una rejilla de tres columnas, en una barra lateral estrecha o a ancho completo dentro del mismo sitio, con el mismo viewport en los tres casos. Diseñar esa tarjeta con media queries obliga a asumir en qué parte del layout va a aparecer, lo cual rompe la idea misma de componente reutilizable.

Las container queries cambian la pregunta: en lugar de “¿cómo de grande es la ventana?”, preguntan “¿cómo de grande es el contenedor de este elemento?”. Para que un elemento pueda consultarse como contenedor, primero hay que declararlo explícitamente con container-type:

.tarjeta-contenedor {
  container-type: inline-size;
  container-name: tarjeta;
}

@container tarjeta (width > 400px) {
  .tarjeta__cuerpo {
    display: grid;
    grid-template-columns: 120px 1fr;
  }
}

@container tarjeta (width <= 400px) {
  .tarjeta__cuerpo {
    display: flex;
    flex-direction: column;
  }
}

container-type: inline-size activa contención de layout, estilo y tamaño en la dimensión horizontal del elemento, que es el caso de uso más común (responder al ancho disponible). El valor size amplía la consulta a ambas dimensiones cuando también importa la altura, aunque es menos frecuente porque impone contención de tamaño más estricta sobre el elemento. El resultado práctico: la misma tarjeta se adapta sola según el espacio real que tiene, sin que el componente necesite saber en qué parte de la página vive ni depender de un ResizeObserver en JavaScript para recalcular clases a mano.

Container queries introducen además unidades relativas al propio contenedor —cqw, cqh, cqi, cqb, cqmin, cqmax— que permiten tipografía o espaciados fluidos respecto al contenedor en lugar de respecto al viewport:

.tarjeta__titulo {
  font-size: clamp(1rem, 0.85rem + 2cqi, 1.75rem);
}

:has(): el selector de padres que CSS nunca tuvo

CSS siempre pudo seleccionar un elemento en función de sus ancestros (.padre .hijo) o de sus hermanos siguientes (.hermano ~ .siguiente), pero nunca al revés: no había forma de decir “aplica este estilo al padre si contiene tal hijo” sin recurrir a JavaScript para añadir una clase manualmente. :has() cierra ese hueco convirtiendo CSS, por primera vez, en un lenguaje capaz de seleccionar “hacia arriba” y “hacia adelante” en el árbol.

/* Estilo distinto para una tarjeta que contiene una imagen */
.tarjeta:has(img) {
  grid-template-rows: auto 1fr;
}

/* Un formulario con algún campo inválido */
form:has(:invalid) {
  border-color: var(--color-error);
}

/* Un label cuyo input está enfocado, sin JavaScript */
label:has(input:focus-visible) {
  outline: 2px solid var(--color-focus);
}

/* Lookahead: un h1 seguido de un h2 */
h1:has(+ h2) {
  margin-bottom: 0.25rem;
}

El caso de form:has(:invalid) merece atención aparte porque sustituye directamente un patrón habitual de JavaScript: escuchar el evento input o change de cada campo, comprobar su validez con la API de validación de formularios y añadir o quitar una clase en el contenedor. Con :has(), ese estado de validación ya es observable en CSS puro, porque :invalid y :has() se combinan de forma nativa.

Según caniuse, :has() tiene soporte estable desde Chrome 105, Edge 105, Safari 15.4 y Firefox 121 (que fue el último de los cuatro grandes motores en habilitarlo por defecto, a finales de 2023), con un soporte global cercano al 95% en 2026. Es, junto con container queries, la funcionalidad de este grupo con el salto de utilidad más claro: convierte en CSS declarativo cosas que antes exigían, sin excepción, una línea de JavaScript.

Nesting nativo: adiós a Sass solo para esto

Anidar selectores fue durante más de una década la razón número uno por la que un proyecto instalaba un preprocesador aunque no necesitara nada más de él. El CSS nativo ya soporta anidamiento con la misma sintaxis que resulta familiar de Sass, usando & para referenciar el selector padre:

.tarjeta {
  padding: 1rem;
  border: 1px solid var(--color-borde);

  & .titulo {
    font-weight: 600;
  }

  &:hover {
    box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
  }

  @container tarjeta (width > 400px) {
    padding: 1.5rem;
  }
}

El detalle relevante no es solo la sintaxis, sino que el navegador la parsea directamente: no hay paso de compilación, no hay .scss que transformar antes de servir el CSS. Esto no vuelve inútil a Sass —sigue aportando variables con lógica más rica, mixins, funciones y bucles que CSS no tiene—, pero sí elimina el caso de uso más habitual por el que un proyecto pequeño lo instalaba: anidar .card .title como .card { .title { ... } } y poco más. El anidamiento nativo alcanzó Baseline “ampliamente disponible” durante 2026, con soporte estable desde Chrome 120, Edge 120, Firefox 117 y Safari 17.2.

Funciones que también cambian cómo se escribe CSS

Container queries y :has() son los cambios más visibles, pero no los únicos que reducen la necesidad de JavaScript o de un preprocesador:

  • color-mix() mezcla dos colores en un espacio de color concreto directamente en CSS, sin precalcular el resultado en una herramienta externa ni mantener una paleta de variantes manuales: color-mix(in oklch, var(--color-primario) 80%, white) genera una variante más clara del color primario sin tocar JavaScript ni un design token adicional. Antes, generar variantes de hover o de estados deshabilitados a partir de un color base solía resolverse con una función de Sass o con una utilidad de JS que devolvía el color calculado; ahora es una línea de CSS que además responde a cambios en la propia variable si se recalcula.
  • clamp(), min() y max(), ya con soporte universal desde hace tiempo, siguen siendo la base de la tipografía y el espaciado fluido, y combinados con unidades de container query (cqi, cqw) permiten que un tamaño de fuente responda al contenedor sin una sola media query.
  • :is() y :where() simplifican selectores que antes había que repetir por cada combinación: :is(h1, h2, h3):has(+ p) en lugar de tres reglas separadas, con :where() como variante que además no añade especificidad, útil para escribir estilos base fáciles de sobreescribir después.

Qué patrones de JavaScript dejan de hacer falta

La pregunta que de verdad importa no es “¿qué CSS es nuevo?” sino “¿qué código dejo de escribir gracias a esto?”. En la práctica:

  • ResizeObserver para layout responsive a nivel de componente. Si el único uso de un ResizeObserver era decidir cuántas columnas mostrar según el ancho disponible de un contenedor, container queries lo sustituyen directamente, sin listeners que limpiar ni recalculos en cada resize.
  • Clases añadidas por JS para reflejar estado del DOM. Formularios con clase has-error puesta a mano tras validar, contenedores con clase has-image añadida condicionalmente al renderizar: si ese estado ya es observable en el propio árbol del DOM (un hijo con :invalid, un hijo <img> presente), :has() lo puede expresar sin JavaScript intermedio.
  • Sass como dependencia obligatoria solo por el anidamiento. Un proyecto que no usaba nada más de Sass que anidar selectores y variables simples puede prescindir del paso de compilación por completo, usando nesting nativo y custom properties de CSS.
  • Funciones JS para variantes de color. Utilidades que calculaban una versión más clara u oscura de un color de marca en tiempo de build o en JavaScript en tiempo de ejecución pueden resolverse con color-mix() directamente en la hoja de estilos, quedando además visibles para cualquiera que lea el CSS sin tener que rastrear una función externa.

Cuándo todavía conviene un preprocesador o una librería

Sass sigue aportando valor real cuando un proyecto necesita lógica más allá de lo declarativo: bucles para generar utilidades repetitivas, funciones matemáticas complejas, un sistema de módulos con @use más maduro que el @import nativo de CSS. Tailwind y otros frameworks de utilidades tampoco quedan obsoletos por esto: resuelven un problema distinto —velocidad de escritura mediante clases atómicas y un sistema de diseño consistente— que container queries y :has() no atacan directamente, aunque Tailwind ya incorpora utilidades para container queries (@container, @sm, etc.) precisamente porque la funcionalidad nativa las hace posibles.

La recomendación práctica para un proyecto nuevo en 2026: da por hecho que container queries, :has(), nesting nativo y color-mix() funcionan en cualquier navegador que tu público use de forma significativa, y solo añade una herramienta externa cuando resuelva algo que estas funciones nativas no resuelven, no por costumbre o porque “así se ha hecho siempre” en el proyecto anterior.

Compartir