Container queries en CSS
En esta lección
- El problema que resuelven
- Paso 1: declarar un contenedor con container-type
- Paso 2: escribir la consulta con @container
- Dar nombre a los contenedores
- Unidades de contenedor: cqi, cqw y compañía
- Style queries: consultar variables CSS
- Media queries frente a container queries
- Ejemplo completo: una tarjeta que se adapta
- Soporte y alternativas
- Errores frecuentes
- Resumen
Las container queries permiten que un componente cambie su diseño según el espacio que tiene su contenedor, en lugar de según el tamaño de la ventana. En CSS moderno viste un adelanto; aquí vas a verlas a fondo: cómo declarar un contenedor, cómo escribir las consultas con @container, cómo poner nombres, las unidades cqi y compañía, las style queries, en qué se diferencian de las media queries y los errores que más tiempo hacen perder. Al final montarás una tarjeta que se adapta sola a cualquier hueco.
El problema que resuelven
Imagina la tarjeta de una ruta de senderismo: foto, título, una frase y un enlace. La usas en tres sitios de la misma página:
- En la columna principal, donde mide 700px: quieres la foto a la izquierda y el texto a la derecha.
- En una barra lateral de 260px: quieres la foto arriba y el texto debajo.
- En una rejilla de tres columnas: depende de lo ancha que sea la ventana.
Con media queries solo puedes preguntar “¿cuánto mide la ventana?”. Pero en una ventana de 1200px, la tarjeta de la columna principal y la de la barra lateral necesitan diseños distintos a la vez. Acabarías escribiendo clases distintas para cada sitio (.tarjeta-lateral, .tarjeta-principal) y repitiendo reglas.
Con container queries la pregunta cambia a “¿cuánto espacio tengo yo?”. La tarjeta se escribe una sola vez y funciona en cualquier sitio. Es la forma natural de pensar en componentes reutilizables.
Paso 1: declarar un contenedor con container-type
Un elemento no es contenedor por defecto. Tienes que convertirlo con la propiedad container-type:
.columna-principal,
.barra-lateral {
container-type: inline-size;
}
Valores posibles:
| Valor | Qué vigila | Cuándo usarlo |
|---|---|---|
inline-size | El ancho (el tamaño en línea) | Casi siempre |
size | Ancho y alto | Solo si el contenedor tiene una altura fija |
normal | Nada de tamaño (valor por defecto) | Para style queries (lo verás más abajo) |
Inline size significa “tamaño en la dirección en que se escribe el texto”. En español, de izquierda a derecha, es el ancho. Si te suena raro, lo tienes explicado en propiedades lógicas.
¿Por qué hay que declararlo? Porque para que el navegador pueda preguntar “cuánto mide el contenedor” sin entrar en un bucle infinito, el ancho del contenedor no puede depender de lo que tiene dentro. Si dependiera, el hijo cambiaría el ancho del padre, el padre cambiaría el estilo del hijo, y vuelta a empezar. Al poner container-type: inline-size, aceptas esa regla: el ancho del contenedor lo deciden su padre y su propio CSS, nunca sus hijos. Esto se llama contención (containment).
Paso 2: escribir la consulta con @container
Con el contenedor declarado, escribes reglas que solo se aplican cuando el contenedor cumple una condición:
.tarjeta {
display: grid;
gap: 12px;
}
@container (width >= 420px) {
.tarjeta {
grid-template-columns: 40% 1fr;
}
}
La tarjeta de la barra lateral (260px) queda en una columna. La de la columna principal (700px) pone la foto a la izquierda. Misma clase, mismo HTML, distinto resultado.
Las condiciones se escriben igual que en las media queries modernas:
@container (width >= 420px) { ... } /* desde 420px */
@container (width < 300px) { ... } /* menos de 300px */
@container (300px <= width < 600px) { ... } /* rango */
@container (width >= 400px) and (height >= 300px) { ... } /* exige size */
@container (orientation: landscape) { ... } /* más ancho que alto, exige size */
@container not (width >= 400px) { ... } /* negación */
También funciona la sintaxis antigua (min-width: 420px), que verás en muchos tutoriales. Las dos son equivalentes.
Las dos reglas de oro
- La consulta mira al contenedor más cercano. El navegador sube desde el elemento por sus antepasados y usa el primero que sea contenedor (del tipo adecuado).
- Una container query nunca cambia al propio contenedor, solo a sus descendientes. Si quisieras cambiar el contenedor según su propio ancho, volverías al bucle infinito.
.barra-lateral { container-type: inline-size; }
/* MAL: el contenedor no puede consultarse a sí mismo */
@container (width < 300px) {
.barra-lateral { padding: 8px; }
}
Esa regla no se aplica a .barra-lateral con su propio tamaño: el navegador buscaría un contenedor por encima de ella. Si necesitas cambiar el “marco”, envuelve el contenido en un elemento interior y aplica los cambios a ese.
Dar nombre a los contenedores
Cuando hay contenedores dentro de otros contenedores, a veces no quieres el más cercano sino uno concreto. Para eso está container-name:
.pagina {
container-type: inline-size;
container-name: pagina;
}
.tarjeta {
container-type: inline-size;
container-name: tarjeta;
}
/* Mira a .pagina, aunque haya contenedores más cerca */
@container pagina (width >= 900px) {
.tarjeta-titulo { font-size: 1.6rem; }
}
El atajo container junta las dos propiedades: primero el nombre, una barra y el tipo.
.barra-lateral {
container: lateral / inline-size;
}
Un elemento puede tener varios nombres separados por espacios (container-name: lateral estrecho;). Elige nombres que describan la zona, no el tamaño: lateral sigue teniendo sentido si mañana la barra se ensancha; pequeno no.
Consejo: pon nombre a tus contenedores desde el principio. Si más adelante alguien añade un contenedor intermedio, tus consultas sin nombre empezarán a mirar al nuevo y el diseño cambiará sin motivo aparente.
Unidades de contenedor: cqi, cqw y compañía
Igual que vw es el 1 % del ancho de la ventana, existen unidades que son un porcentaje del contenedor:
| Unidad | Equivale a |
|---|---|
cqw | 1 % del ancho del contenedor |
cqh | 1 % del alto del contenedor (exige size) |
cqi | 1 % del tamaño en línea (en español, el ancho) |
cqb | 1 % del tamaño en bloque (en español, el alto; exige size) |
cqmin | El menor entre cqi y cqb |
cqmax | El mayor entre cqi y cqb |
En la práctica, cqi es la que más usarás. La diferencia con cqw solo se nota en idiomas que se escriben en vertical, así que cqi es la opción más correcta en general.
Su uso estrella es la tipografía fluida dentro de un componente, combinada con clamp():
.tarjeta-titulo {
/* nunca menos de 1.1rem ni más de 1.8rem; en medio, el 6 % del contenedor */
font-size: clamp(1.1rem, 6cqi, 1.8rem);
}
.tarjeta {
padding: clamp(8px, 4cqi, 24px);
}
Si el contenedor mide 300px, 6cqi son 18px; si mide 500px, 30px. El título crece con la tarjeta, no con la ventana.
Dos detalles:
- Las unidades se calculan respecto al contenedor más cercano del elemento. Si el propio elemento es contenedor, sus unidades
cq*miran al contenedor de encima. - Si no hay ningún contenedor por encima, las unidades usan la ventana (como
svwysvh). No da error, pero el resultado no será el que esperas.
Style queries: consultar variables CSS
Además del tamaño, @container puede preguntar por el valor de una variable CSS del contenedor. Se llaman style queries:
.zona-ofertas {
--tema: destacado;
}
@container style(--tema: destacado) {
.tarjeta {
border: 2px solid #c2410c;
background: #fff7ed;
}
}
Todas las tarjetas que estén dentro de .zona-ofertas se pintan como destacadas, sin añadir clases a cada una. Para las style queries no hace falta container-type: todos los elementos son contenedores de estilo por defecto.
¿Para qué sirven si ya existen las clases? Para que un componente reaccione al contexto sin que el contexto tenga que conocer sus clases internas. El que monta la página solo pone --tema: destacado en la zona, y cada componente decide qué significa eso para él.
Nota: de momento solo se pueden consultar variables CSS (las que empiezan por
--), no propiedades normales comocolor. Su soporte llegó más tarde que el de las consultas de tamaño: compruébalo en caniuse.com antes de usarlas en un proyecto real y trátalas como una mejora que puede no aplicarse.
Media queries frente a container queries
No se sustituyen: se complementan.
| Media queries | Container queries | |
|---|---|---|
| Preguntan por | La ventana o el dispositivo | Un elemento antepasado |
| Se escriben con | @media | @container |
| Necesitan preparar algo | No | container-type en el contenedor |
| Pueden consultar | Ancho, alto, orientación, preferencias del usuario (prefers-color-scheme…), tipo de puntero | Ancho, alto, orientación del contenedor; variables CSS |
| Unidades propias | vw, vh, dvh… | cqi, cqb, cqw… |
| Ideales para | El esqueleto de la página: columnas, menú, cabecera | Componentes reutilizables: tarjetas, widgets, formularios |
Una forma sencilla de decidir: si la pregunta es sobre la página (“¿el menú cabe en horizontal?”), usa @media. Si es sobre una pieza (“¿esta tarjeta tiene sitio para la foto al lado?”), usa @container. Y las preferencias del usuario (modo oscuro, movimiento reducido) siempre van con @media.
Ejemplo completo: una tarjeta que se adapta
Vamos a montar la tarjeta de ruta en tres diseños según su espacio: estrecha (foto arriba), media (foto a la izquierda) y ancha (foto a la izquierda y datos en columnas).
<div class="pagina">
<main class="principal">
<article class="ruta">
<img src="cares.jpg" alt="Desfiladero del Cares con el río al fondo">
<div class="ruta-texto">
<h2 class="ruta-titulo">Ruta del Cares</h2>
<p>Un sendero excavado en la roca entre Poncebos y Caín.</p>
<ul class="ruta-datos">
<li>12 km</li>
<li>Dificultad media</li>
<li>4 h</li>
</ul>
</div>
</article>
</main>
<aside class="lateral">
<article class="ruta">
<img src="lagos.jpg" alt="Lago Enol rodeado de prados">
<div class="ruta-texto">
<h2 class="ruta-titulo">Lagos de Covadonga</h2>
<p>Circular sencilla entre el Enol y el Ercina.</p>
<ul class="ruta-datos">
<li>6 km</li>
<li>Fácil</li>
<li>2 h</li>
</ul>
</div>
</article>
</aside>
</div>
/* Esqueleto de la página: media query */
.pagina {
display: grid;
gap: 24px;
}
@media (width >= 900px) {
.pagina { grid-template-columns: 1fr 280px; }
}
/* Contenedores */
.principal,
.lateral {
container: zona / inline-size;
}
/* Componente: diseño base (estrecho) */
.ruta {
display: grid;
gap: 12px;
background: white;
border-radius: 8px;
overflow: hidden;
}
.ruta img {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
display: block;
}
.ruta-texto { padding: 0 16px 16px; }
.ruta-titulo {
margin: 0;
font-size: clamp(1.1rem, 5cqi, 1.8rem);
}
.ruta-datos {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* Diseño medio: foto a la izquierda */
@container zona (width >= 420px) {
.ruta { grid-template-columns: 40% 1fr; }
.ruta img { height: 100%; aspect-ratio: auto; }
.ruta-texto { padding: 16px 16px 16px 0; }
}
/* Diseño ancho: datos en tres columnas iguales */
@container zona (width >= 650px) {
.ruta-datos {
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
}
}
Fíjate en el reparto de trabajo: la media query decide si la barra lateral va al lado o debajo; las container queries deciden cómo se dibuja cada tarjeta en el hueco que le ha tocado. En un móvil, la tarjeta de la barra lateral ocupa todo el ancho y se ve igual de bien que la principal. En un ordenador, cada una toma un diseño distinto sin una sola clase extra.
En el ejemplo de esta lección (botón “Pruébalo”) tienes una versión con un contenedor que puedes estirar con el ratón gracias a resize: horizontal. Es la mejor forma de ver la consulta en acción.
Soporte y alternativas
Las container queries de tamaño y las unidades cq* funcionan en todos los navegadores actuales desde 2023. Si tienes que dar servicio a navegadores muy antiguos, escribe primero un diseño base que funcione sin ellas (la versión estrecha) y deja las mejoras dentro de @container: el navegador que no lo entienda simplemente ignorará esas reglas. Si alguna vez necesitas comprobarlo de forma explícita, existe @supports (container-type: inline-size) { ... }.
Para depurar, abre DevTools: en el panel Elements, los contenedores llevan una etiqueta container, y al seleccionar un elemento afectado por una @container, el panel Styles muestra la consulta y a qué contenedor apunta.
Errores frecuentes
- Olvidar
container-type. Sin él,@containerno encuentra contenedor y la regla nunca se aplica. - Querer cambiar el propio contenedor. Las consultas solo afectan a los descendientes; envuelve el contenido en otro elemento.
- El contenedor que se encoge a cero. Con
container-type: inline-size, el ancho ya no depende del contenido. Si el contenedor es un elemento flotante,inline-blocko un hijo flex sin ancho definido, puede quedarse en 0px y el contenido se desborda. Dale un ancho (width,flex: 1, una columna de Grid…). - Usar
sizesin altura.container-type: sizetambién ignora el contenido para el alto; si no le das una altura, el contenedor mide 0px de alto. - Consultas sin nombre con contenedores anidados. La consulta mira al más cercano, que quizá no es el que pensabas. Usa
container-name. - Usar
@containerpara todo. El esqueleto de la página y las preferencias del usuario siguen siendo cosa de@media. - Unidades
cqisin contenedor. Funcionan, pero miden la ventana, y el texto no se adapta al componente.
Resumen
| Herramienta | Para qué | Ejemplo |
|---|---|---|
container-type: inline-size | Convertir un elemento en contenedor de ancho | .lateral { container-type: inline-size; } |
container-name | Elegir un contenedor concreto | container-name: lateral; |
container | Atajo de nombre y tipo | container: lateral / inline-size; |
@container | Estilos según el tamaño del contenedor | @container (width >= 420px) { ... } |
@container nombre | Consultar un contenedor con nombre | @container lateral (width < 300px) |
cqi, cqw, cqb, cqh | Medidas relativas al contenedor | font-size: clamp(1rem, 5cqi, 1.6rem); |
style() | Consultar una variable CSS del contenedor | @container style(--tema: destacado) |
@media | Esqueleto de la página y preferencias | @media (width >= 900px) |
Las container queries hablan de “tamaño en línea” y “tamaño en bloque”. En la siguiente lección verás de dónde vienen esos nombres: propiedades lógicas.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.