Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoAvançadocódigo testado

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.

Rodolfo Mori11 min de leitura

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.

html
<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:

css
.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:

#c1 coluna 240px flex-direction: row texto: 142.4px altura do titulo: 100.78125px #c2 coluna 390px flex-direction: row texto: 252.5px altura do titulo: 67.1875px #c3 coluna 610px flex-direction: row texto: 264.9px altura do titulo: 33.59375px

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.

css
.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:

css
.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; }
selo-a sem container-type largura 100.6px selo-b container-type: inline-size largura 18.0px caixa-a sem container-type altura 24.4px caixa-b container-type: size altura 2.0px

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:

css
.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:

viewport: 1440px de largura (uma tela só)

#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:

body — sem container-type .coluna — container-type: inline-size .livro @container (min-width: 320px) a consulta sobe largura consultada

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:

Três colunas lado a lado na mesma tela mostrando o mesmo card de livro em estados diferentes: empilhado na coluna de 206px, horizontal com capa pequena na de 356px e horizontal com capa grande e sinopse na de 576px

E o mesmo relatório de estilo calculado, tirado da própria página do print:

#c1 206px flex-direction: column capa: 206px sinopse: none #c2 356px flex-direction: row capa: 96px sinopse: none #c3 576px flex-direction: row capa: 150px sinopse: block

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):

css
.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; }
}
prateleira: 574px de inline-size ficha (#l1): 253px de inline-size

@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:

css
.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; }
viewport: 1440 x 900px

#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:

viewport: 1440px 50cqi sem nenhum conteiner ancestral = 720px

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.

css
.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:

.livro e o proprio conteiner e mede 576px .livro flex-direction: column (a regra NAO pegou) .livro__capa width: 96px (a regra pegou)

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.

css
.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.

css
.coluna {
  width: 610px;
  padding: 16px;
  /* faltou o container-type aqui */
}

@container (min-width: 320px) {
  .livro { flex-direction: row; }
  .livro__capa { width: 96px; }
}
.coluna mede 576px, mas nao declarou container-type .livro flex-direction: column .livro__capa width: 576px

Nada aconteceu, mesmo com 576px disponíveis. E o console:

(console vazio: nenhum aviso, nenhum erro)

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:

js
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:

c1 1cqi = 2.06px | inline-size = 206px c2 1cqi = 3.56px | inline-size = 356px c3 1cqi = 5.76px | inline-size = 576px

Na página quebrada aqui de cima, a mesma sonda imprime outra coisa:

c1 1cqi = 14.40px | inline-size = 1440px

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:

css
.selo { display: none; }

@container (320px <= width < 520px) {
  .selo { display: block; }
}
viewport: 1440px

#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:

css
.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; } }
container-type: inline-size (400px) and:block or:block not:block min-height:none aspect-ratio:none container-type: size (400x300px) and:block or:block not:block min-height:block aspect-ratio: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.

css
.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; }
}
nenhuma prateleira tem container-type declarado --densidade: confortavel padding: 20px sinopse: block --densidade: compacta padding: 6px sinopse: 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.

  • css
  • container queries
  • componente
  • responsivo
  • cqi

Perguntas frequentes

Container query substitui media query?
Não substitui, divide o trabalho. Media query continua sendo a ferramenta certa para decisões da página inteira e para o que não é largura — impressão, tema escuro, prefers-reduced-motion. Container query é para o layout interno de um componente que pode aparecer em vários lugares.
Preciso criar uma div a mais só para ser o contêiner?
Quase sempre sim, e essa é a parte chata. Como o elemento consultado não pode ser o próprio contêiner, você precisa de um ancestral. Na prática dá para reaproveitar o que já existe — o item de grid, o li da lista, a section da barra lateral — em vez de inventar um wrapper novo.
container-type deixa a página mais lenta?
O efeito costuma ser o contrário: a contenção diz ao navegador que aquele pedaço da árvore pode ser recalculado sozinho. O que dói é o efeito colateral no layout, não o custo de processamento — um elemento que se dimensionava pelo conteúdo para de fazer isso.
Dá para animar a mudança entre dois estados de container query?
Dá. Trocar padding, gap ou largura da capa faz transição normalmente, porque são propriedades interpoláveis. display não interpola, mas mesmo assim entra na transição hoje: com transition-behavior allow-discrete o navegador segura o display block até o fim da animação, e com @starting-style o fade de entrada aparece inteiro em vez de estourar de uma vez.
Posso usar cqi no lugar de vw para tipografia fluida?
Pode, e em componente reutilizável é melhor: o texto passa a crescer com a caixa em que ele está, não com a janela. Só combine com clamp(), porque 8cqi sozinho fica minúsculo dentro de uma coluna estreita.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — CSS container queries — developer.mozilla.org
  2. CSS Containment Module Level 3 — w3.org
  3. MDN — container-type — developer.mozilla.org

Continue por aqui