wandres.dev
CONTAINER QUERIES · Consultar el contenedor, no la ventana

Por qué el viewport era la pregunta equivocada

Las element queries se pidieron durante diez años y nadie las implementó porque eran matemáticamente imposibles tal y como se pedían. Entender ese fracaso es entender por qué las container queries son como son.

⏱ 18 min

Entre 2013 y 2020, “element queries” fue la petición más repetida de la comunidad de CSS y la que más rotundamente rechazaron los implementadores. No era desidia ni falta de recursos: la propuesta ingenua contenía una dependencia circular que ningún motor puede resolver, y cualquier implementación honesta tenía que romper el círculo de alguna forma. Las container queries son la forma en que la especificación decidió romperlo, y por eso vienen con una restricción que parece arbitraria hasta que conoces el problema que evita.

🎯 Al terminar esta lección sabrás
  • Formular con precisión el bucle infinito que hace imposible la element query ingenua.
  • Explicar por qué los polyfills basados en JavaScript no resolvían el problema, solo lo movían.
  • Situar el papel de ResizeObserver y por qué su especificación habla de bucles.
  • Enunciar el compromiso que aceptaron las container queries para ser implementables.

La petición y su trampa

La petición era razonable: “quiero aplicar estilos a un elemento según el ancho de ese elemento”. Escrita en pseudocódigo sería algo así:

/* Esto NO existe y no puede existir */
.tarjeta (width < 400px) {
  .tarjeta { display: block; }
  .tarjeta img { inline-size: 100%; }
}

El problema aparece en cuanto los estilos aplicados pueden cambiar el ancho del propio elemento. Imagina una tarjeta con inline-size: fit-content cuyo ancho depende de su contenido. Si mide 380px, la condición se cumple, y la regla pone la imagen al 100% y apila el contenido, lo que hace que la tarjeta pase a medir 420px. Con 420px la condición deja de cumplirse, los estilos se retiran, la tarjeta vuelve a 380px, y la condición se cumple otra vez.

El navegador no oscilaría visualmente: se quedaría colgado. El cálculo de layout no converge, y un motor no puede entregar un frame hasta que converge. No hay heurística aceptable: limitar las iteraciones produce resultados no deterministas que dependen del orden de evaluación, y detectar el ciclo en el caso general es tan caro como resolverlo.

Esto no es un caso rebuscado. Cualquier elemento cuyo tamaño dependa de su contenido —que en CSS son casi todos, porque el tamaño intrínseco es el comportamiento por defecto— puede entrar en el bucle.

Por qué los polyfills no lo resolvían

Todas las librerías de element queries de aquella década funcionaban igual: medir con JavaScript, añadir una clase, dejar que el navegador recalculara. El bucle no desaparecía, simplemente pasaba de ser un problema del motor a ser un problema tuyo, con dos agravantes.

El primero, de corrección: la medida ocurre después del layout, así que el usuario ve un frame con el estilo equivocado antes de la corrección. En una carga de página eso es un salto visible, y en las métricas de estabilidad visual se paga.

El segundo, de terminación: si tus estilos cambiaban el tamaño medido, el bucle seguía existiendo, solo que ahora consumía un frame por vuelta en lugar de colgar el hilo. La página se quedaba parpadeando a 60 oscilaciones por segundo hasta que alguien tocaba el código.

ResizeObserver, estandarizado en 2020, formalizó el problema en vez de esconderlo. Su algoritmo procesa las notificaciones en profundidades crecientes del árbol y, si tras una pasada quedan cambios pendientes en elementos ya notificados, dispara un evento de error ResizeObserver loop limit exceeded y abandona el resto hasta el siguiente frame. Es decir: el estándar admite explícitamente que el bucle puede ocurrir y define cómo rendirse de forma controlada. Ese error, que mucha gente ha visto en sus registros sin saber qué significaba, es el fantasma de las element queries.

ℹ️
Un problema viejo con otro nombre

La dependencia circular entre estilo y layout no es nueva en CSS y ya se resolvió antes por decreto: las media queries no pueden consultar la altura del contenido, :hover no puede mover el elemento fuera del cursor sin consecuencias raras, y los porcentajes de height requieren un ancestro con altura definida precisamente para no depender del contenido que contienen. Cada vez que la plataforma se ha encontrado con este patrón, la solución ha sido la misma: exigir que la cantidad consultada esté determinada antes que lo que depende de ella.

El compromiso

La solución de las container queries es directa y consiste en aceptar una restricción a cambio de la funcionalidad: para poder consultar el tamaño de un elemento, ese elemento debe declarar que su tamaño no depende de su contenido en el eje consultado. Eso es lo que hace container-type, y el mecanismo que lo garantiza es el containment, que veremos con detalle en la lección siguiente.

Con esa garantía, el bucle desaparece porque desaparece la realimentación: el motor calcula el tamaño del contenedor a partir de su propio contexto —lo que le dé su padre—, evalúa la condición, aplica los estilos y sabe con certeza que nada de lo que ocurra dentro puede cambiar el número que acaba de usar.

De esa restricción se derivan las dos características de las container queries que más desconciertan al principio y que son puras consecuencias lógicas, no decisiones de diseño arbitrarias. Que un elemento no pueda consultarse a sí mismo, porque los estilos que aplicaría afectan a su propio contenido. Y que container-type: size haga que un elemento sin altura declarada colapse a cero, porque le has dicho al motor que ignore su contenido al calcular la altura, y sin contenido y sin altura declarada no queda nada.

Lo que esto cambia en la arquitectura de componentes

El cambio profundo no es sintáctico. Hasta ahora, un componente CSS no podía ser una unidad autocontenida: para adaptarse necesitaba información —el ancho de la ventana— que era global, y por tanto tenía que coordinarse con todo lo demás. Un componente que se adapta al viewport no es reutilizable, es reutilizable en las condiciones en que fue diseñado.

Con la consulta de contenedor, el componente pasa a depender solo de lo que su padre le da, que es exactamente el mismo contrato que ya usas en cualquier sistema de componentes de JavaScript: recibes props, no lees variables globales. Un componente escrito así se puede colocar en una portada, en una barra lateral, en un modal y en una vista de impresión sin que su autor tuviera que conocer ninguno de esos sitios.

Ese es el motivo por el que esto se considera el cambio de layout más importante de la década, muy por encima de cualquier propiedad nueva: no añade una capacidad, elimina una dependencia global.

Las restricciones de una API son su parte más informativa

Cuando una especificación te obliga a hacer algo que parece burocrático —declarar container-type, envolver el componente en un div extra— la reacción normal es buscar cómo saltárselo. La reacción productiva es la contraria: preguntarse qué imposibilidad está comprando esa obligación. Aquí la respuesta es limpia: container-type no es una anotación para que el navegador sepa a qué elemento mirar —eso lo podría deducir solo—, es una promesa que tú firmas de que el tamaño de ese elemento es independiente de su contenido, y el navegador la hace cumplir por la fuerza aplicando containment. A cambio de esa promesa obtienes una garantía de terminación que ningún polyfill pudo dar en diez años. Este patrón se repite en toda la plataforma y en todo el diseño de sistemas: los tipos en un compilador, los readonly en una base de datos, la inmutabilidad en un modelo de datos, las funciones puras en un motor de reactividad. En todos los casos la estructura es la misma —renuncias a expresar algo, y a cambio el sistema puede razonar sobre lo que queda—, y en todos los casos quien intenta rodear la restricción acaba reconstruyendo a mano el problema que la restricción evitaba.