Ir para o conteúdo
React

CSS Container Queries na Prática: Componentes Responsivos Sem JavaScript

Marcos Soares
Atualizado em 
13 minutos de leitura
Ilustracao 3D de paineis de vidro fosco com layouts adaptativos representando CSS Container Queries
Ouça este artigo
0:00CSS Container Queries na Prática: Componentes Responsivos Sem JavaScript--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

O problema que media queries nunca resolveram

Um card de produto precisa funcionar em três contextos: sidebar de 280px, grid de duas colunas e hero de largura total. Com media queries, você escreve breakpoints baseados na viewport. O card não sabe se está numa sidebar ou num grid: ele só sabe a largura da janela do navegador.

O resultado é previsível: você acaba criando variantes com classes CSS (.card--small, .card--large) ou, pior, usando ResizeObserver em JavaScript para medir o container pai e aplicar classes dinamicamente. Container queries resolvem isso na camada certa: o componente reage ao espaço que o container pai oferece, não à viewport.

Como container queries funcionam por baixo

O mecanismo depende de dois conceitos: containment context e container query.

Primeiro, você declara um elemento como container de consulta. Isso instrui o navegador a rastrear as dimensões desse elemento para que filhos possam consultá-las. Segundo, você escreve regras @container que funcionam como media queries, mas consultam o container ancestral mais próximo.

CSS
/* Declara o wrapper como container nomeado */
.product-list {
  container-type: inline-size;
  container-name: product-list;
}
 
/* O card filho consulta o container, não a viewport */
@container product-list (min-width: 600px) {
  .product-card {
    display: grid;
    grid-template-columns: 200px 1fr;
    gap: 1.5rem;
  }
}

container-type: inline-size ativa containment apenas no eixo inline (horizontal em LTR). Existe size para ambos os eixos, mas raramente você precisa dele: containment em ambos os eixos exige que o elemento tenha altura explícita, o que quebra layouts com conteúdo dinâmico.

Container query vs. media query: quando usar cada uma

CritérioMedia queryContainer query
Referência de medidaViewport do navegadorElemento container pai
Caso de uso principalLayouts de página (header, grid global, nav)Componentes reutilizáveis (cards, widgets, forms)
ComposiçãoNão compõe: mesmo breakpoint para todos os contextosCompõe: cada instância reage ao próprio container
Suporte (2024+)Universal96%+ dos navegadores (baseline desde dezembro 2023)
Eixo verticalFunciona com min-heightRequer container-type: size e altura explícita
PerformanceSem custo extra de layoutCusto marginal de containment (imperceptível em cenários reais)

A regra prática: use media queries para o layout da página e container queries para componentes que vivem dentro desse layout. As duas coexistem sem conflito.

Card de produto responsivo ao container

Este é o caso de uso mais comum. O card empilha imagem sobre texto quando o container é estreito e muda para layout horizontal quando há espaço.

CSS
/* container.css */
.card-container {
  container-type: inline-size;
  container-name: card-wrapper;
}
 
.product-card {
  display: flex;
  flex-direction: column;
  border: 1px solid hsl(220 10% 90%);
  border-radius: 0.5rem;
  overflow: hidden;
}
 
.product-card__image {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  width: 100%;
}
 
.product-card__body {
  padding: 1rem;
}
 
.product-card__title {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}
 
/* Quando o container tem pelo menos 500px, layout horizontal */
@container card-wrapper (min-width: 500px) {
  .product-card {
    flex-direction: row;
  }
 
  .product-card__image {
    /* Limita a imagem a 40% do espaço horizontal para
       manter proporção legível no texto ao lado */
    width: 40%;
    aspect-ratio: 1 / 1;
  }
 
  .product-card__title {
    font-size: 1.25rem;
  }
}
 
/* Container largo: card vira destaque com tipografia maior */
@container card-wrapper (min-width: 800px) {
  .product-card__title {
    font-size: 1.5rem;
  }
 
  .product-card__body {
    padding: 1.5rem 2rem;
  }
}
HTML
<!-- O mesmo card funciona em qualquer contexto sem classes extras -->
<div class="card-container" style="width: 300px;">
  <article class="product-card">
    <img class="product-card__image" src="/img/teclado.webp" alt="Teclado mecânico" />
    <div class="product-card__body">
      <h3 class="product-card__title">Teclado Mecânico TKL</h3>
      <p>Switch brown, hot-swap, iluminação RGB por tecla.</p>
    </div>
  </article>
</div>
 
<!-- Mesmo HTML, container diferente: layout muda automaticamente -->
<div class="card-container" style="width: 700px;">
  <article class="product-card">
    <img class="product-card__image" src="/img/teclado.webp" alt="Teclado mecânico" />
    <div class="product-card__body">
      <h3 class="product-card__title">Teclado Mecânico TKL</h3>
      <p>Switch brown, hot-swap, iluminação RGB por tecla.</p>
    </div>
  </article>
</div>

Coloque os dois blocos na mesma página e redimensione a janela. O card de 300px permanece empilhado enquanto o de 700px fica horizontal. Zero JavaScript.

Sidebars são o segundo caso clássico. Quando a sidebar encolhe (por toggle do usuário ou por layout de grid), os componentes dentro dela precisam se adaptar.

CSS
/* sidebar-layout.css */
.dashboard-layout {
  display: grid;
  grid-template-columns: var(--sidebar-width, 280px) 1fr;
  min-height: 100dvh;
}
 
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
  background: hsl(220 15% 97%);
  transition: width 0.2s ease;
}
 
/* Estado colapsado controlado por CSS (checkbox hack ou :has) */
.sidebar[data-collapsed="true"] {
  --sidebar-width: 64px;
  width: 64px;
}
 
.nav-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
}
 
.nav-item__label {
  white-space: nowrap;
  overflow: hidden;
}
 
/* Quando a sidebar fica estreita, esconde labels e centraliza ícones */
@container sidebar (max-width: 100px) {
  .nav-item {
    justify-content: center;
    padding: 0.75rem;
  }
 
  .nav-item__label {
    /* display:none causaria problemas de acessibilidade;
       sr-only pattern mantém o texto para screen readers */
    position: absolute;
    width: 1px;
    height: 1px;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    overflow: hidden;
  }
}

O componente de navegação não precisa saber se a sidebar está colapsada. Ele consulta o espaço disponível e se adapta. Isso é o que torna container queries superiores a classes toggladas por JavaScript: a responsabilidade de layout fica inteiramente no CSS.

Container query units: tipografia fluida por container

Além de breakpoints, container queries trazem unidades relativas ao container: cqw (1% da largura do container), cqh (1% da altura), cqi (inline), cqb (block).

CSS
.hero-banner {
  container-type: inline-size;
}
 
.hero-banner__title {
  /* Tipografia que escala com o container, não com a viewport.
     clamp garante limites legíveis em containers muito
     pequenos ou muito grandes */
  font-size: clamp(1.25rem, 4cqi, 3rem);
  line-height: 1.2;
}
 
.hero-banner__subtitle {
  font-size: clamp(0.875rem, 2.5cqi, 1.5rem);
  color: hsl(220 10% 40%);
}

Compare com vw (viewport width): se você usar 4vw no título, ele fica enorme em telas largas independentemente do container. Com 4cqi, o título escala proporcionalmente ao espaço que o banner realmente ocupa. Se o banner está numa sidebar de 300px, o título fica pequeno. Se ocupa a página inteira, fica grande.

Shorthand container e containers aninhados

CSS
/* Shorthand: container-name / container-type */
.outer {
  container: outer / inline-size;
}
 
.inner {
  container: inner / inline-size;
}
 
/* Consulta o container mais próximo por padrão */
@container (min-width: 400px) {
  .widget {
    padding: 2rem;
  }
}
 
/* Consulta um container específico pelo nome */
@container outer (min-width: 900px) {
  .widget {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}

Quando você não especifica nome na regra @container, o navegador resolve para o ancestor mais próximo que tenha container-type definido. Nomear containers é boa prática em layouts com aninhamento, porque evita ambiguidade.

O que NÃO fazer

Anti-pattern 1: container-type: size sem altura explícita

CSS
/* ERRADO: containment em ambos os eixos sem altura definida */
.card-wrapper {
  container-type: size;
  /* Sem height definido, o browser colapsa a altura para 0
     porque containment bloqueia o conteúdo de influenciar
     as dimensões do container */
}

O resultado: o container desaparece. O navegador precisa de uma altura explícita quando você ativa containment no eixo block.

CSS
/* CORRETO: use inline-size (suficiente para 95% dos casos) */
.card-wrapper {
  container-type: inline-size;
}
 
/* Ou, se realmente precisa de containment vertical,
   defina altura explícita */
.card-wrapper-with-height {
  container-type: size;
  height: 400px;
}

Anti-pattern 2: aplicar container-type no próprio elemento que consulta

CSS
/* ERRADO: o card é container de si mesmo */
.product-card {
  container-type: inline-size;
}
 
@container (min-width: 500px) {
  .product-card {
    /* Isso não funciona como esperado: o card consulta
       seu próprio container, que é ele mesmo, criando
       referência circular. O browser ignora a query. */
    flex-direction: row;
  }
}

Container queries consultam um ancestor. O elemento que declara container-type nunca é o alvo das suas próprias queries.

CSS
/* CORRETO: container no pai, query no filho */
.product-card-wrapper {
  container-type: inline-size;
}
 
@container (min-width: 500px) {
  .product-card {
    flex-direction: row;
  }
}

Anti-pattern 3: substituir todas as media queries por container queries

Media queries continuam sendo a ferramenta certa para decisões de layout de página. Trocar @media (max-width: 768px) por container query no <body> não traz benefício: você está consultando a viewport indiretamente, com overhead de containment.

Usando container queries com design systems em React

Se você trabalha com design systems baseados em Radix UI e Tailwind, container queries se integram via @tailwindcss/container-queries (plugin oficial do Tailwind).

TSX
// ProductCard.tsx
// Plugin @tailwindcss/container-queries ativo no tailwind.config
export function ProductCard({ title, image, description }: ProductCardProps) {
  return (
    // @container marca o wrapper como containment context
    <div className="@container">
      <article className="flex flex-col @md:flex-row border rounded-lg overflow-hidden">
        <img
          src={image}
          alt={title}
          className="w-full @md:w-2/5 aspect-video @md:aspect-square object-cover"
        />
        <div className="p-4 @md:p-6">
          <h3 className="text-base @md:text-xl @lg:text-2xl font-semibold">
            {title}
          </h3>
          <p className="mt-2 text-sm text-gray-600">{description}</p>
        </div>
      </article>
    </div>
  );
}
TypeScript
// tailwind.config.ts
import containerQueries from "@tailwindcss/container-queries";
import type { Config } from "tailwindcss";
 
const config: Config = {
  content: ["./src/**/*.{ts,tsx}"],
  plugins: [containerQueries],
};
 
export default config;

Os prefixos @md: e @lg: funcionam como os breakpoints responsivos do Tailwind, mas consultam o container ao invés da viewport. O componente se adapta ao espaço disponível, o que faz diferença real quando você testa componentes isolados com ferramentas como Playwright e Vitest: o card se comporta corretamente independentemente do viewport do teste.

Container queries e CSS nativo: o que muda na arquitetura

Container queries fazem parte de uma tendência maior do CSS nativo assumindo responsabilidades que antes exigiam JavaScript. Se você leu sobre scroll-driven animations e corner-shape, o padrão é o mesmo: a plataforma web está eliminando a necessidade de bibliotecas JS para layout e animação.

Isso tem impacto direto na arquitetura de componentes. Um card que antes precisava de um ResizeObserver com callback para trocar classes agora funciona com CSS declarativo. Menos JavaScript significa menos bundle, menos re-renders e menos bugs de timing (o ResizeObserver dispara assincronamente, o que pode causar flash de layout incorreto).

Para quem constrói APIs e camadas fullstack, a consequência prática é que o frontend fica mais leve e previsível. Componentes com container queries não precisam de estado React para decidir layout: o CSS resolve sozinho.

Suporte e fallback pragmático

O suporte a container queries atingiu baseline em dezembro de 2023. Chrome 105+, Firefox 110+, Safari 16+. Se o seu público inclui navegadores corporativos presos em versões antigas, use @supports:

CSS
/* Fallback: layout empilhado funciona em qualquer navegador */
.product-card {
  display: flex;
  flex-direction: column;
}
 
/* Progressive enhancement: só aplica se container queries existem */
@supports (container-type: inline-size) {
  .card-wrapper {
    container-type: inline-size;
  }
 
  @container (min-width: 500px) {
    .product-card {
      flex-direction: row;
    }
  }
}

O fallback é o layout mobile/empilhado. Funciona em 100% dos navegadores. A versão com container query é progressive enhancement.

Exemplo avançado: dashboard com widgets responsivos

Um cenário real onde Container Queries brilham: um dashboard onde widgets ocupam espaços variáveis dependendo da configuração do usuário.

CSS
/* dashboard.css */
 
.dashboard-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
 
.widget-slot {
  container-type: inline-size;
  container-name: widget;
}
 
.widget {
  background: #fff;
  border-radius: 8px;
  padding: 1rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
 
.widget__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}
 
.widget__chart {
  /* Altura mínima para o gráfico não colapsar */
  min-height: 150px;
}
 
.widget__stats {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.5rem;
}
 
/* Widget com mais espaço: stats lado a lado */
@container widget (min-width: 450px) {
  .widget__stats {
    grid-template-columns: repeat(3, 1fr);
  }
 
  .widget__chart {
    min-height: 250px;
  }
}
 
/* Widget em coluna larga: layout com sidebar de filtros */
@container widget (min-width: 700px) {
  .widget__content {
    display: grid;
    grid-template-columns: 1fr 200px;
    gap: 1rem;
  }
}
TSX
// Widget.tsx
import styles from './Widget.module.css';
 
interface WidgetProps {
  title: string;
  children: React.ReactNode;
}
 
export function Widget({ title, children }: WidgetProps) {
  return (
    <div className={styles.slot}>
      <section className={styles.widget}>
        <div className={styles.header}>
          <h2 className={styles.title}>{title}</h2>
        </div>
        {children}
      </section>
    </div>
  );
}

O dashboard pode ter 1, 2, 3 ou 4 colunas. Os widgets se adaptam ao slot que ocupam. Se o usuário arrasta um widget para uma coluna mais larga, ele automaticamente mostra mais informação. Isso é impossível de fazer bem com media queries porque a viewport não muda quando o layout do grid muda.

Combinando com CSS moderno

Container Queries funcionam bem com outras features CSS recentes. Se você já experimentou animações de scroll com CSS puro, a ideia é similar: delegar ao CSS o que antes exigia JavaScript.

CSS
/* Combine container queries com :has() para
   estilização condicional baseada em conteúdo */
.card-wrapper {
  container-type: inline-size;
}
 
/* Card com imagem ganha layout diferente em contêineres largos.
   :has() verifica a presença da imagem no DOM. */
@container (min-width: 400px) {
  .card:has(.card__image) {
    grid-template-columns: 200px 1fr;
  }
 
  .card:not(:has(.card__image)) {
    /* Sem imagem, o card usa todo o espaço para texto */
    padding: 1.5rem;
  }
}

FAQ

Container queries substituem media queries?

Não. Media queries continuam sendo a ferramenta certa para layouts de página (quando o grid principal muda de 3 colunas para 1, por exemplo). Container queries resolvem o problema de componentes reutilizáveis que aparecem em contextos de tamanhos diferentes. Use as duas juntas.

Posso usar container queries com CSS Modules ou styled-components?

CSS Modules funcionam sem nenhuma adaptação: container-type e @container são CSS padrão. Styled-components e Emotion também suportam, desde que você escreva a regra @container dentro do template literal. O único cuidado é garantir que o container esteja no componente pai, não no próprio componente que consulta.

Container queries afetam performance?

O custo é o containment de layout. Quando você declara container-type: inline-size, o navegador isola o cálculo de layout daquele elemento. Na prática, isso é uma otimização: o browser faz menos recálculos de layout, não mais. Em páginas com centenas de containers (dashboards com muitos widgets), o impacto continua imperceptível segundo benchmarks do time do Chrome.

Como debugar container queries no DevTools?

Chrome DevTools mostra um badge "container" no elemento que declara container-type. Ao inspecionar um elemento filho com regras @container, o painel de estilos mostra a query e indica se está ativa ou não. Firefox tem suporte similar. Redimensionar o container no DevTools (editando width inline) é a forma mais rápida de testar breakpoints.

Tailwind suporta container queries nativamente?

A partir do Tailwind v3.2+, o plugin oficial @tailwindcss/container-queries adiciona suporte completo. No Tailwind v4 (em desenvolvimento), container queries devem ter suporte nativo sem plugin. Os prefixos @sm:, @md:, @lg: seguem a mesma lógica dos breakpoints responsivos.

A posição que defendo

Container queries são a feature de CSS mais relevante desde Grid Layout. Elas resolvem um problema real que existia desde o início da web responsiva: componentes que precisam se adaptar ao contexto, não à viewport. Se você ainda está usando ResizeObserver para trocar classes de layout, está escrevendo JavaScript que o CSS já resolve melhor, com menos código, sem re-renders e sem flash de conteúdo.

A adoção deveria ser imediata para qualquer projeto que não precisa suportar IE ou Safari 15. O fallback com @supports é trivial. O ganho em manutenibilidade é concreto: um componente, zero variantes de classe, zero JavaScript de layout. Isso é o que "componente verdadeiramente responsivo" significa.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.