Container queries: o componente responde ao pai
Como fazer um card mudar de layout pela largura do contêiner e não da tela, com container-type, @container e as unidades cqw, cqi e cqb.
Container query é uma regra CSS que pergunta pela largura de um ancestral do elemento, e não pela largura da janela. Com ela, o mesmo card de livro vira horizontal numa coluna larga e vertical numa coluna estreita — na mesma tela, no mesmo instante, sem uma linha de JavaScript.
Todos os exemplos daqui são de uma livraria online, a Aurora. O componente é sempre o mesmo: a ficha de um livro, com capa, título, autor, sinopse e preço.
O problema que a media query nunca resolveu
A vitrine da Aurora tem três colunas de larguras diferentes na mesma página: uma barra lateral de 240px, uma coluna do meio de 390px e um destaque de 610px. O mesmo componente entra nas três.
<section class="coluna">
<article class="livro">
<div class="livro__capa"></div>
<div class="livro__dados">
<h2>Grande Sertão: Veredas</h2>
<p>João Guimarães Rosa</p>
<strong>R$ 79,90</strong>
</div>
</article>
</section>O CSS começa empilhado e, a partir de uma tela grande, tenta virar o card de lado:
.livro {
display: flex;
flex-direction: column;
gap: 12px;
}
.livro__capa {
width: 100%;
aspect-ratio: 2 / 3;
}
@media (min-width: 700px) {
.livro { flex-direction: row; }
.livro__capa { width: 96px; }
}Abri essa página no Chromium com a janela em 1440px e li o flex-direction
calculado de cada uma das três colunas:
As três viraram row, inclusive a barra lateral de 240px. Repare na última
coluna do relatório: o título ocupa 33,6px de altura no destaque (uma linha) e
100,8px na barra lateral (três linhas), espremido em 142px de largura. A media
query respondeu certo a uma pergunta errada. Ela sabe o tamanho da janela; o
que decide o layout do card é o tamanho da coluna.
Esse é o limite estrutural: media query é global. Nenhuma combinação de
min-width conserta isso, porque a informação de que o componente precisa nem
chega até ela. Se você ainda não viu a mecânica de @media, vale ler
media queries no CSS antes de seguir — o resto
desta lição assume que a sintaxe já é familiar.
container-type: inline-size, e o preço que ele cobra
Para um elemento poder ser consultado, ele precisa se declarar contêiner. Isso é
o container-type.
.coluna {
container-type: inline-size;
}inline-size quer dizer “eu respondo perguntas sobre a minha largura”. Existe
também size, que responde sobre largura e altura, e normal, que é o padrão e
não responde sobre tamanho nenhum.
Só que declarar contêiner não é grátis. container-type liga contenção: uma
promessa ao navegador de que o tamanho daquele elemento não depende do que tem
dentro dele. Coloquei um selo inline-block e uma caixa de altura automática nas
duas versões e medi:
.selo { display: inline-block; padding: 8px; border: 1px solid #333; }
#selo-b { container-type: inline-size; }
.caixa { width: 300px; border: 1px solid #333; }
#caixa-b { container-type: size; }O selo desabou de 100,6px para 18px — sobrou o padding e a borda, porque o texto “Frete grátis” parou de contar para a largura. A caixa desabou de 24,4px para 2px pelo mesmo motivo, agora no eixo vertical.
Na prática, inline-size é o que você vai usar em 95% dos casos, e ele é seguro
em qualquer elemento cuja largura já vem de fora: item de grid, filho de flex com
flex: 1, bloco de largura total. Todos esses casos são o normal num layout
feito com Flexbox ou com Grid.
@container: a pergunta sobe até o ancestral
Com o contêiner declarado, a regra @container funciona igual a uma media query
— só que a largura consultada é a do contêiner mais próximo:
.livro {
display: flex;
flex-direction: column;
gap: 12px;
}
.livro__capa { width: 100%; aspect-ratio: 2 / 3; flex: none; }
.livro__sinopse { display: none; }
@container (min-width: 320px) {
.livro { flex-direction: row; }
.livro__capa { width: 96px; }
}
@container (min-width: 520px) {
.livro__capa { width: 150px; }
.livro__sinopse { display: block; }
}Mesma página, mesma janela de 1440px, mesmas três colunas:
#c1 contêiner 206px flex-direction: column capa: 206px sinopse: none #c2 contêiner 356px flex-direction: row capa: 96px sinopse: none #c3 contêiner 576px flex-direction: row capa: 150px sinopse: block
Três estados diferentes ao mesmo tempo. Os 206px, 356px e 576px são as larguras internas das colunas — 240, 390 e 610 menos a borda e os 16px de padding de cada lado. É esse número, o content box do contêiner, que a consulta compara.
O caminho que a pergunta faz é sempre para cima, nunca para baixo nem para o lado:
Três larguras na mesma tela, com um CSS só
Esta é a regra do bloco anterior renderizada, com um acabamento visual em cima, numa janela de 1440×900 no Chromium 151. Nenhuma media query participa da imagem:

E o mesmo relatório de estilo calculado, tirado da própria página do print:
Guarde esse resultado como critério de decisão: se a resposta certa depende de onde o componente foi colocado, é container query. Se depende do dispositivo em que a página inteira está sendo lida, é media query. Os dois convivem no mesmo arquivo sem conflito.
Dar nome ao contêiner para escolher quem responde
Quando existe contêiner dentro de contêiner, a consulta sem nome pega o mais próximo. Isso às vezes é exatamente o que você não quer. Na Aurora, a prateleira é um contêiner (574px) e cada ficha dentro dela é outro (253px):
.prateleira {
container: prateleira / inline-size;
display: grid;
grid-template-columns: 1fr 1fr;
}
.livro {
container: ficha / inline-size;
}
.livro__sinopse,
.livro__ordenar { display: none; }
/* sem nome: cai na ficha, o contêiner mais próximo */
@container (min-width: 400px) {
.livro__sinopse { display: block; }
}
/* com nome: pula a ficha e pergunta para a prateleira */
@container prateleira (min-width: 400px) {
.livro__ordenar { display: block; }
}@container (min-width: 400px) .livro__sinopse display: none @container prateleira (min-width: 400px) .livro__ordenar display: block
A sinopse ficou escondida porque a ficha tem 253px. O botão de reordenar
apareceu porque a prateleira tem 574px. Mesmo limite de 400px, duas respostas
diferentes, e a única diferença é a palavra prateleira na consulta.
container: prateleira / inline-size é o atalho de container-name mais
container-type. Nomeie sempre que houver aninhamento: consulta sem nome dentro
de componente aninhado é a receita para um bug que só aparece quando alguém
reusar o card num lugar novo.
cqw, cqi, cqb: medir em porcentagem do contêiner
Junto com @container vieram unidades novas, todas relativas ao contêiner de
consulta e não à janela:
| unidade | 1% de quê | equivale, na escrita horizontal |
|---|---|---|
cqw |
largura do contêiner | igual a cqi |
cqh |
altura do contêiner | igual a cqb |
cqi |
inline-size do contêiner | a largura |
cqb |
block-size do contêiner | a altura |
cqmin |
o menor entre cqi e cqb |
— |
cqmax |
o maior entre cqi e cqb |
— |
Coloquei uma sonda de width: 100cq* em cada uma das três colunas e um título em
font-size: 8cqi:
.coluna { container-type: inline-size; }
.sonda--cqw { width: 100cqw; }
.sonda--cqi { width: 100cqi; }
.sonda--cqb { width: 100cqb; }
.sonda--cqmin { width: 100cqmin; }
.sonda--cqmax { width: 100cqmax; }
.titulo { font-size: 8cqi; }#c1 contêiner 206px de inline-size 1cqw = 2.06px 1cqi = 2.06px 1cqb = 9.00px 1cqmin = 2.06px 1cqmax = 9.00px font-size do título (8cqi) = 16.48px #c2 contêiner 356px de inline-size 1cqw = 3.56px 1cqi = 3.56px 1cqb = 9.00px 1cqmin = 3.56px 1cqmax = 9.00px font-size do título (8cqi) = 28.48px #c3 contêiner 576px de inline-size 1cqw = 5.76px 1cqi = 5.76px 1cqb = 9.00px 1cqmin = 5.76px 1cqmax = 9.00px font-size do título (8cqi) = 46.08px
O mesmo 8cqi produziu 16,48px, 28,48px e 46,08px — a tipografia acompanhou a
caixa, não a janela. Isso é o que vw nunca conseguiu fazer dentro de um
componente, e combina bem com clamp(), como está em
tipografia no CSS.
Agora repare no 1cqb: deu 9,00px nas três colunas. Não é coincidência — é 1% da
altura da janela, que era 900px. Como o contêiner é inline-size, ele não tem
altura para responder, e a unidade cai para o viewport pequeno. O efeito
colateral é que cqmax também escolheu 9,00px, um número que não tem nada a ver
com o contêiner. Isso vale para qualquer cq* sem contêiner nenhum acima:
50cqi virou 720px, metade da janela. É um fallback silencioso: não dá erro, só
dá o número errado.
Se as unidades relativas ainda são um terreno movediço, a lição de
unidades no CSS cobre rem, em, % e vw com a
mesma régua de medição.
O contêiner não consegue se estilizar pela própria consulta
Este é o erro que mais derruba gente na primeira tentativa: colocar
container-type no próprio card e esperar que ele mude de layout.
.livro {
container-type: inline-size;
display: flex;
flex-direction: column;
}
@container (min-width: 320px) {
.livro { flex-direction: row; }
.livro__capa { width: 96px; }
}Com o card medindo 576px, largura de sobra para passar dos 320px:
A capa mudou, o card não. A regra @container só alcança os descendentes do
contêiner, nunca o contêiner em si — e a razão é circular: se .livro pudesse se
redimensionar a partir de uma consulta sobre a própria largura, a nova largura
mudaria a resposta da consulta, que mudaria a largura de novo. O CSS corta esse
laço proibindo a auto-consulta.
A correção é sempre a mesma: mover container-type para um ancestral.
.coluna { container-type: inline-size; }
.livro { display: flex; flex-direction: column; }
@container (min-width: 320px) {
.livro { flex-direction: row; }
}Quando o navegador simplesmente ignora a sua @container
O segundo erro é mais cruel, porque não deixa rastro: escrever a @container
perfeita e esquecer o container-type no pai.
.coluna {
width: 610px;
padding: 16px;
/* faltou o container-type aqui */
}
@container (min-width: 320px) {
.livro { flex-direction: row; }
.livro__capa { width: 96px; }
}Nada aconteceu, mesmo com 576px disponíveis. E o console:
Sem contêiner válido acima, a consulta nunca é verdadeira e o CSS segue em
frente calado. Para diagnosticar em dez segundos, cole isto no console do
DevTools com a página aberta — a sonda mede 1cqi de cada contêiner e some
depois:
document.querySelectorAll('.coluna').forEach((coluna) => {
const sonda = document.createElement('div');
sonda.style.cssText = 'width:100cqi;height:0';
coluna.append(sonda);
const cqi = sonda.getBoundingClientRect().width / 100;
sonda.remove();
console.log(coluna.id, '1cqi =', cqi.toFixed(2) + 'px', '| inline-size =', cqi * 100 + 'px');
});Na vitrine em que as três colunas declararam container-type, ela imprime a
largura real de cada contêiner:
Na página quebrada aqui de cima, a mesma sonda imprime outra coisa:
1440px é a largura da janela, não os 576px da coluna. É o mesmo fallback
silencioso da seção anterior, agora trabalhando a seu favor: o número fora de
escala é a assinatura do container-type que faltou.
A sintaxe de faixa também funciona
@container aceita a mesma comparação de intervalo das media queries modernas —
útil para um estado que só existe no meio da escala:
.selo { display: none; }
@container (320px <= width < 520px) {
.selo { display: block; }
}#c1 240px .selo display: none #c2 390px .selo display: block #c3 610px .selo display: none
Só a coluna do meio acendeu o selo. (Aqui as colunas não têm padding, então a largura do contêiner é a largura da coluna.)
Dentro de @container também valem and, or e not, e dá para consultar
outras propriedades além de width. Testei as cinco formas nos dois tipos de
contêiner:
.a { container-type: inline-size; width: 400px; }
.b { container-type: size; width: 400px; height: 300px; }
p { display: none; }
@container (min-width: 300px) and (max-width: 500px) { .e1 { display: block; } }
@container (max-width: 100px) or (min-width: 300px) { .e2 { display: block; } }
@container not (min-width: 900px) { .e3 { display: block; } }
@container (min-height: 200px) { .e4 { display: block; } }
@container (aspect-ratio > 1) { .e5 { display: block; } }A lógica funciona igual nos dois. O que muda é o que pode ser perguntado:
min-height e aspect-ratio só respondem em container-type: size, porque um
contêiner inline-size não sabe a própria altura. E, como você viu na segunda
seção, ligar size num elemento de altura automática colapsa ele.
Consulta de estilo: perguntar por uma variável, não por largura
A parte mais nova é a style(): em vez de perguntar pela largura, você pergunta
pelo valor de uma custom property no ancestral. Serve para variação de tema e de
densidade, que não têm nada a ver com tamanho.
.prateleira { --densidade: confortavel; }
.prateleira--compacta { --densidade: compacta; }
.livro { padding: 20px; }
.livro__sinopse { display: block; }
@container style(--densidade: compacta) {
.livro { padding: 6px; }
.livro__sinopse { display: none; }
}Repare na primeira linha do relatório: nenhuma prateleira declarou
container-type, e a consulta funcionou mesmo assim. Todo elemento já é
contêiner de estilo por padrão — só o de tamanho precisa ser declarado, porque
só ele cobra contenção. É o que torna style() barato de adotar num projeto que
já usa variáveis CSS.
O que ainda não dá para fazer é consultar propriedades comuns como
style(display: flex): hoje a consulta de estilo só enxerga custom properties.
E o suporte é mais recente que o das consultas de tamanho — teste nos navegadores
que o seu projeto atende antes de depender dela para o layout, e mantenha o
estado padrão utilizável quando a regra não pegar.
O que vem depois
O jeito prático de adotar container query num projeto que já existe é começar
pelo componente que aparece em mais de um lugar — card, banner de aviso, item de
lista — e transformar as media queries dele em @container, deixando as media
queries de página inteira onde estão. Você vai descobrir que metade dos
min-width do seu CSS era sobre a caixa, não sobre a tela.
O passo seguinte na trilha de CSS é dar movimento a essas
mudanças de estado com transition e transform. Se quiser rever a ordem
inteira antes, o guia de CSS mostra onde cada assunto entra.
Perguntas frequentes
Container query substitui media query?
Preciso criar uma div a mais só para ser o contêiner?
container-type deixa a página mais lenta?
Dá para animar a mudança entre dois estados de container query?
Posso usar cqi no lugar de vw para tipografia fluida?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Chromium 151.0.7922.34 via Playwright 1.62.1, Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — CSS container queries — developer.mozilla.org
- CSS Containment Module Level 3 — w3.org
- MDN — container-type — developer.mozilla.org


