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

Containment: el aislamiento que hace posible la pregunta

Qué promete cada tipo de containment, por qué el motor necesita aislar el subárbol para responder a una container query sin entrar en bucle, y qué te cobra a cambio.

⏱ 20 min

El containment es un contrato entre tú y el motor: le dices que lo que pase dentro de un subárbol no puede afectar a lo de fuera, y a cambio él puede tratar ese subárbol como una caja negra y saltárselo cuando recalcula el resto del documento. Se especificó como optimización de rendimiento años antes de las container queries, y resultó ser la pieza exacta que faltaba para hacerlas implementables. Sin entender qué promesa firmas al escribir container-type, las container queries se quedan en sintaxis.

🎯 Al terminar esta lección sabrás
  • Enumerar los cuatro tipos de containment y qué garantiza cada uno.
  • Explicar en términos del algoritmo de layout por qué el containment rompe el bucle.
  • Distinguir qué containment aplica hoy container-type y cuál dejó de aplicar en 2024.
  • Decidir entre size e inline-size sabiendo qué se rompe con cada uno.

Los cuatro tipos

La propiedad contain acepta cuatro valores independientes que se pueden combinar. Cada uno corta un tipo de influencia entre el subárbol y el exterior.

size. El tamaño del elemento se calcula como si no tuviera contenido. Es la promesa fuerte: el motor puede determinar las dimensiones del elemento sin mirar dentro. Consecuencia inmediata: un elemento con contain: size y sin dimensiones declaradas mide cero en los dos ejes.

inline-size. Igual que la anterior pero solo en el eje en línea. El tamaño de bloque sigue saliendo del contenido. Es la variante útil en la práctica, porque el caso normal es un elemento cuyo ancho viene impuesto por el padre y cuya altura crece con lo que hay dentro.

layout. El layout interno es independiente del externo y viceversa. El elemento establece un contexto de formato independiente: los flotantes no salen ni entran, los márgenes no colapsan con los de fuera, y el motor puede recalcular el interior sin volver a tocar el exterior. Trae además dos efectos secundarios que sorprenden: el elemento se convierte en bloque contenedor de los descendientes absolute y fixed, y crea un contexto de apilamiento. Retén ese detalle, porque la tercera sección trata precisamente de por qué container-type ya no aplica este tipo.

style. Ciertas propiedades con efectos que se propagan hacia arriba —los contadores de CSS y quotes— quedan confinadas al subárbol. Es el tipo con menos consecuencias visibles y el que menos se usa a mano.

Existe además paint, que garantiza que nada del subárbol se pinta fuera de los límites del elemento, y dos atajos: contain: content equivale a layout paint style, y contain: strict equivale a layout paint style size.

Cómo rompe el bucle

Recuerda la circularidad: los estilos que aplica una consulta pueden cambiar el tamaño que la consulta lee. Con containment de tamaño, esa flecha desaparece del grafo de dependencias.

flowchart TB
A[El padre asigna un ancho al contenedor] --> B[containment de tamano en el eje consultado]
B --> C[El ancho del contenedor queda fijado sin mirar dentro]
C --> D[Se evalua la condicion del container]
D --> E[Se aplican los estilos a los descendientes]
E --> F{Ese cambio puede alterar el ancho del contenedor}
F -->|No hay containment| G[El ancho cambia y hay que reevaluar la condicion]
G --> H[Bucle que no converge]
F -->|Con containment| I[El ancho no depende del contenido]
I --> J[Layout resuelto en una pasada]
style A fill:#89b4fa,color:#11111b
style B fill:#cba6f7,color:#11111b
style J fill:#a6e3a1,color:#11111b
style H fill:#f38ba8,color:#11111b

Puesto en términos del algoritmo: el layout de una caja se resuelve calculando primero su tamaño disponible a partir del contexto —el bloque contenedor, las restricciones del padre— y después distribuyendo el contenido dentro. Normalmente el segundo paso puede modificar el resultado del primero, porque el tamaño intrínseco del contenido participa en el cálculo del tamaño de la caja. El containment de tamaño elimina esa participación: el tamaño del contenedor queda determinado antes de que empiece el layout de sus hijos, y por tanto es un valor estable que la condición puede leer.

Es la misma idea que la estratificación por fases en un compilador: si la fase B puede modificar la entrada de la fase A, no puedes ejecutarlas en orden y necesitas un punto fijo. Prohibiendo esa modificación, el orden vuelve a estar bien definido.

Lo que te cobra container-type, y lo que dejó de cobrarte

Aquí hay un cambio de comportamiento reciente que conviene conocer entero, porque casi toda la documentación que vas a encontrar —incluida a día de hoy la lista de contextos de apilamiento de MDN— sigue describiendo el modelo antiguo.

Lo que aplica hoy. El texto vigente de css-conditional-5 es explícito: container-type: size aplica containment de estilo y de tamaño; container-type: inline-size aplica containment de estilo y de tamaño en el eje en línea; y los dos establecen un contexto de formato independiente. Eso es toda la lista. No hay containment de layout.

Lo que aplicaba antes. Hasta 2024, container-type aplicaba además containment de layout, y de ahí salían dos efectos secundarios que rompían layouts sin previo aviso. El contenedor se convertía en bloque contenedor de los descendientes absolute y fixed, con lo que un modal con position: fixed dentro de una tarjeta consultable dejaba de anclarse al viewport. Y el contenedor creaba un contexto de apilamiento, con lo que un z-index: 9999 de dentro no podía pasar por encima de nada de fuera.

Por qué se revirtió. El grupo de trabajo concluyó que el containment de layout era bastante más fuerte de lo que las container queries necesitaban, y que esa fuerza de más estaba bloqueando otras cosas: impedía alinear por línea base a través del contenedor, impedía que los posicionados escaparan de él y impedía que el desbordamiento desplazable de los hijos contase. De todo el paquete, lo único que las container queries necesitaban de verdad era el contexto de formato independiente. Así que la resolución fue quedarse solo con eso y soltar el resto.

Cuándo cambió. Chrome 129 en septiembre de 2024, Firefox 133 y Safari 18.4. Los motores llegaron antes que el texto de la especificación, que es el orden habitual en CSS.

Lo que sigue en pie es el contexto de formato independiente —los márgenes de los hijos no colapsan con los del contenedor y los flotantes de fuera no invaden el interior— y, sobre todo, el containment de tamaño, que es la parte que hace posible la pregunta y la que te cobra el colapso a cero cuando eliges size sin darle altura.

Si necesitas el comportamiento antiguo, y a veces lo necesitas porque tu layout se apoyaba en él sin saberlo, hoy hay que pedirlo de forma explícita:

.panel {
  container-type: inline-size;
  contain: layout;      /* recupera el contexto de apilamiento y el bloque contenedor */
}

Y si solo quieres el bloque contenedor sin el contexto de apilamiento, position: relative en el contenedor hace exactamente eso.

⚠️
Un diagnóstico que caducó

Durante dos años, “se me ha roto el modal al declarar el contenedor” fue un diagnóstico correcto y muy repetido. Ya no lo es en ningún navegador actual, así que conviene desaprenderlo: cuando hoy un position: fixed no se ancla al viewport, el culpable que hay que buscar hacia arriba en el árbol es un transform, un filter o un contain, no un container-type. Y la trampa simétrica es peor, porque es silenciosa: si tu layout se apoyaba en que el contenedor capturase a los descendientes posicionados, ese apoyo desapareció bajo tus pies al actualizar el navegador y nadie te avisó.

size frente a inline-size

container-type: size permite consultar altura, anchura y proporción, y aplica containment en los dos ejes. inline-size permite consultar solo el eje en línea y aplica containment solo ahí.

En el 95% de los casos quieres inline-size, y la razón es estructural: los documentos web crecen en el eje de bloque. Un contenedor con size y sin altura declarada mide cero de alto, con lo que su contenido se sale entero y el resultado es inutilizable. Solo tiene sentido cuando el contenedor ya tiene una altura definida por otro medio: una celda de rejilla con pista fija, un panel con block-size declarado, un elemento con aspect-ratio.

/* correcto y habitual */
.tarjeta-envoltorio { container-type: inline-size; }

/* correcto solo porque la altura viene de fuera */
.celda { block-size: 20rem; container-type: size; }
@container (block-size < 12rem) { .celda .detalle { display: none; } }

El coste de rendimiento va en la dirección opuesta a la intuición: declarar un contenedor no es caro por sí mismo, y de hecho el containment que aplica suele hacer el layout más barato al acotar el trabajo de recálculo. Lo que sí cuesta es declarar contenedores a lo bruto sobre miles de elementos, porque cada uno añade una entrada más a la búsqueda de contenedores que el motor hace al resolver cada @container. La recomendación práctica es declarar contenedores donde hay una consulta que los use, no por si acaso.

Containment convirtió una optimización en una capacidad

Lo más instructivo de esta historia es cronológico. contain se especificó alrededor de 2015 con un objetivo puramente de rendimiento: dar a los desarrolladores una forma de decirle al motor “puedes saltarte este subárbol”, para que una aplicación con miles de nodos no pagara un recálculo global por cada cambio local. Nadie lo diseñó pensando en consultas. Pero al escribir esa promesa de aislamiento de forma precisa, la especificación creó sin querer exactamente el invariante que hacía falta para que una consulta sobre el tamaño de un elemento fuera decidible. Cuando años después se retomó el problema de las element queries, la solución no requirió inventar nada nuevo: requirió darse cuenta de que la pieza ya estaba en la caja. Esto ocurre constantemente en el diseño de sistemas y merece la pena tenerlo presente: las garantías precisas son composables de formas que sus autores no anticiparon, y las heurísticas no lo son. Un mecanismo que dice “normalmente esto es más rápido” no habilita nada nuevo; uno que dice “el tamaño de esta caja no depende de su contenido” es un teorema pequeño, y sobre teoremas pequeños se pueden construir cosas grandes.

⚔️ Provoca y observa el aislamiento
  1. Declara contain: size en un div con contenido y sin altura, y comprueba en el inspector que su caja mide cero.
  2. Mete un position: fixed dentro de un container-type: inline-size y comprueba que sigue anclado al viewport. Añade contain: layout al contenedor y comprueba que ahora deja de estarlo.
  3. Comprueba que los márgenes de un hijo dejan de colapsar con los del contenedor al declararlo: eso es el contexto de formato independiente, y ese no se fue.
  4. Coloca un hijo con z-index: 9999 dentro del contenedor y un hermano del contenedor con z-index: 1: verifica que el hijo queda encima, y que solo deja de quedarlo si añades contain: layout.
  5. Compara en el panel de rendimiento el tiempo de layout de una lista larga con y sin contain: content en cada fila.