Un contenedor no puede consultarse a sí mismo
La restricción central del modelo, por qué el containment no la elimina, el patrón del envoltorio y las tres trampas que se encuentra todo el mundo al aplicarlo.
Es la primera piedra con la que tropieza todo el mundo: declaras container-type en tu tarjeta, escribes la consulta, y los estilos no se aplican a la tarjeta. No es un fallo de implementación ni una limitación temporal a la espera de mejoras: es la consecuencia inevitable del mismo razonamiento que hizo posible la característica, y entender exactamente por qué te ahorra buscar durante meses un truco que no existe.
- Explicar por qué el containment de tamaño no basta para permitir la autoconsulta.
- Aplicar el patrón del envoltorio y saber dónde poner cada estilo.
- Evitar las tres trampas habituales:
display: contents, el envoltorio como ítem de layout y el contenedor mal elegido. - Decidir entre envoltorio,
:has()y atributos según lo que estés modelando.
Por qué no basta con el containment
El razonamiento parece sólido: si container-type: inline-size garantiza que el ancho del elemento no depende de su contenido, entonces los estilos aplicados dentro no pueden cambiar ese ancho, y por tanto podría consultarse a sí mismo sin bucle. La conclusión es falsa, y el error está en confundir dos cosas distintas.
El containment de tamaño hace que el ancho del elemento sea independiente de su contenido. No lo hace independiente de sus propias propiedades. Y una regla dentro de la consulta podría cambiar las propiedades del propio contenedor:
/* si esto fuera legal, seria circular */
.tarjeta { container-type: inline-size; inline-size: fit-content; }
@container (inline-size > 30rem) {
.tarjeta { padding-inline: 4rem; font-size: 1.5rem; }
}
padding, border, font-size, display, flex-basis, grid-column, inline-size y una docena más de propiedades del propio elemento participan en el cálculo de su ancho. Si la consulta pudiera modificarlas, volveríamos al bucle exacto de las element queries, containment incluido. La única forma de cortarlo sin enumerar una lista arbitraria de propiedades prohibidas es la que eligió la especificación: la consulta se resuelve contra un ancestro, y solo puede afectar a descendientes de ese ancestro.
Es la misma disciplina que aplican los sistemas reactivos bien diseñados: un efecto no puede escribir en el valor del que depende. La alternativa es prohibir propiedades caso por caso, y eso produce reglas que nadie recuerda y motores que discrepan entre sí.
El patrón del envoltorio
La solución práctica es separar el elemento que es contenedor del elemento que reacciona:
<div class="tarjeta-wrap">
<article class="tarjeta">
<img class="tarjeta__media" src="..." alt="">
<div class="tarjeta__cuerpo">...</div>
</article>
</div>
.tarjeta-wrap { container: tarjeta / inline-size; }
.tarjeta {
display: grid;
gap: 0.75rem;
padding: 1rem;
border-radius: 0.75rem;
}
@container tarjeta (inline-size > 26rem) {
.tarjeta {
grid-template-columns: 12rem 1fr;
align-items: start;
padding: 1.25rem;
}
.tarjeta__media { block-size: 100%; object-fit: cover; }
}
El envoltorio no lleva ningún estilo visual: ni fondo, ni borde, ni padding. Su único trabajo es recibir un ancho de su padre y ofrecerlo como referencia. Todo lo visible vive en .tarjeta, que ahora sí puede cambiar de todo porque es descendiente.
Este reparto tiene una ventaja que no se ve al principio: el envoltorio es también el punto natural donde el consumidor del componente decide cuánto espacio le da. La tarjeta ya no negocia su tamaño con el mundo, lo recibe.
Las tres trampas del envoltorio
Trampa 1: display contents en el envoltorio
La reacción inmediata de quien no quiere un elemento extra es hacerlo transparente al layout:
/* NO funciona */
.tarjeta-wrap { display: contents; container: tarjeta / inline-size; }
Un elemento con display: contents no genera caja principal, y sin caja no hay tamaño que consultar. El container-type no tiene sobre qué actuar. La consulta se evaluará contra el siguiente ancestro que sí sea contenedor, o contra ninguno, y en ambos casos el resultado será distinto del que esperabas y no habrá ningún aviso.
Si el envoltorio te molesta porque rompe una rejilla —el caso real detrás de esta tentación—, la solución correcta es que el envoltorio sea el ítem de la rejilla y la tarjeta viva dentro de él, no eliminarlo.
Trampa 2: el envoltorio que no recibe el ancho que crees
El envoltorio hereda su ancho de su padre, y como es un div de bloque, por defecto ocupa el 100% del ancho disponible. Eso es lo que quieres. Deja de serlo en dos situaciones.
Si el envoltorio es ítem de un contenedor flex, su ancho lo determina el algoritmo de flex, y con flex: 0 1 auto el ancho saldrá del contenido… salvo que el containment lo impide, con lo que puede colapsar. La corrección es darle una base explícita:
.lista { display: flex; flex-wrap: wrap; gap: 1rem; }
.lista > .tarjeta-wrap { flex: 1 1 16rem; container: tarjeta / inline-size; }
Si el envoltorio está dentro de una rejilla con pistas auto o max-content, la pista intentará dimensionarse según el contenido, y el contenido está contenido. Usa pistas con tamaño definido o 1fr cuando dentro haya contenedores de consulta.
Trampa 3: haber elegido mal el contenedor
El error más caro no es sintáctico. Consiste en declarar el contenedor en un ancestro lejano —la sección, el main, el body— y consultar desde ahí. Funciona, y a cambio devuelve el componente al mismo acoplamiento del que querías salir: ahora depende del ancho de la sección, que es casi tan global como el viewport.
La pregunta correcta al elegir el contenedor es: ¿cuál es el elemento cuyo ancho determina de verdad cómo debe verse esto? Para una tarjeta de una galería es su celda. Para un widget de barra lateral es la barra. Para un formulario es el panel que lo alberga. Cuanto más cerca esté el contenedor del componente, más portable es el componente.
Envoltorio, has() o atributo
No todo lo que parece necesitar autoconsulta la necesita. Hay tres modelados distintos y conviene distinguirlos.
Si la condición es sobre el espacio disponible, es una container query con envoltorio. No hay alternativa.
Si la condición es sobre el contenido —“esta tarjeta tiene imagen”, “este formulario tiene un campo con error”—, la herramienta es :has(), que sí permite estilar el propio elemento porque no crea ninguna circularidad de tamaño:
.tarjeta:has(.tarjeta__media) { grid-template-rows: auto 1fr; }
Si la condición es sobre una decisión de quien usa el componente —“esta tarjeta va en versión compacta”—, no es una consulta de nada: es un atributo o una clase, y meterlo en una container query es sobreingeniería.
Cuesta aceptar un elemento sin más función que existir, sobre todo si vienes de una cultura de marcado minimalista. Pero ese div está codificando una separación real: quién decide el espacio y quién lo consume. En los sistemas de componentes modernos esa separación ya existe con otros nombres —el contenedor que aplica el layout frente al componente que se renderiza dentro— y hacerla explícita en el DOM cuesta un nodo y ahorra la clase entera de bugs donde un componente se estilaba a sí mismo en función de un tamaño que él mismo influía.
Da un paso atrás y mira la forma del problema, porque la vas a reconocer en sitios que no tienen nada que ver con CSS. Tienes un valor observado —el tamaño—, una regla que reacciona a él y un efecto que podría modificarlo. Ese triángulo es exactamente el que produce bucles infinitos en un useEffect que actualiza su propia dependencia, en un watch de un framework reactivo que escribe en lo que observa, en un trigger de base de datos que actualiza la tabla que lo dispara y en un ResizeObserver que cambia el tamaño del elemento observado. Todos los sistemas que sobreviven a este patrón lo resuelven con la misma medicina: estratificar. Se establece una dirección permitida para el flujo —de arriba abajo en el árbol, del observado al observador, de la fase N a la fase N más uno— y se prohíbe estructuralmente la flecha de vuelta, en lugar de intentar detectar los ciclos cuando ocurren. La prohibición se siente arbitraria mientras no has visto el ciclo, y se siente sabia en cuanto lo has visto una vez. Si algún día diseñas un sistema donde algo observa un estado y puede modificarlo, ya sabes cuál es la única solución que escala: no detectar el bucle, hacerlo inexpresable.
- Implementa la tarjeta con envoltorio y comprueba que la consulta la afecta.
- Quita el envoltorio, declara
container-typeen la propia tarjeta y verifica que la consulta deja de aplicar. - Pon
display: contentsen el envoltorio y comprueba en el inspector contra qué elemento se resuelve ahora la consulta. - Mete el envoltorio como ítem de un flex sin base explícita y observa el colapso; corrígelo con
flex: 1 1 16rem. - Coloca la misma tarjeta en una galería de tres columnas, en una barra lateral estrecha y en un modal ancho sin tocar su CSS.