Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
ComparaçãoIntermediáriocódigo testado

Flexbox ou Grid: qual usar em cada tipo de layout

A diferença real entre os dois, o critério de uma dimensão contra duas, e os mesmos quatro layouts montados nas duas técnicas para comparar.

Rodolfo Mori19 min de leitura

A regra curta é esta: Flexbox organiza uma fila, Grid organiza uma tabela. Se o que você tem é uma sequência de itens num eixo só — uma barra, uma linha de botões, o miolo de um cartão — é Flexbox. Se você precisa que colunas e linhas se alinhem entre si, é Grid.

O problema é que quase todo layout parece caber nos dois. Para sair da opinião, montei os quatro layouts mais comuns de um site nas duas técnicas e medi o resultado num navegador de verdade: Chromium 151 dirigido pelo Playwright, num MacBook, com as posições lidas por getBoundingClientRect().

O site medido é o da Livraria Bento, uma livraria de bairro colocando o catálogo no ar: barra de navegação, vitrine de livros, formulário do clube de assinatura e a página inteira com filtros na lateral. É o mesmo domínio do começo ao fim.

Toda medição deste artigo parte da mesma base, que vale para os dois lados da comparação e não é repetida nos blocos seguintes:

css
* { box-sizing: border-box; }
body { margin: 0; font: 16px/1.4 Arial, sans-serif; }
a { text-decoration: none; color: #111; }

O box-sizing: border-box importa: é ele que faz padding e border caberem dentro da largura declarada. Sem ele, todo número abaixo muda.

Uma dimensão contra duas: o critério que decide

A frase “Flexbox é uma dimensão, Grid é duas” é repetida sem explicação em quase todo lugar. Ela quer dizer uma coisa bem concreta: em Flexbox, cada linha é independente das outras. O contêiner distribui os itens ao longo do eixo, e quando acaba o espaço ele começa uma linha nova que não sabe nada sobre a anterior. Em Grid, as colunas são definidas antes, no pai, e valem para todas as linhas ao mesmo tempo.

Flexbox — uma dimensão cada linha se arruma sozinha as colunas não existem Grid — duas dimensões as colunas são do contêiner e valem para todas as linhas

Dá para ver isso em número. A Livraria Bento tem seis etiquetas de gênero, com textos de tamanhos diferentes, dentro de uma caixa de 620 px:

html
<div class="estantes">
  <span class="etiqueta">Ficção brasileira</span>
  <span class="etiqueta">Poesia</span>
  <span class="etiqueta">Infantojuvenil</span>
  <span class="etiqueta">História</span>
  <span class="etiqueta">Quadrinhos</span>
  <span class="etiqueta">Ensaio</span>
</div>
css
.estantes {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  width: 620px;
  padding: 16px;
}

.etiqueta { border: 1px solid #ccc; padding: 8px 12px; }
css
.estantes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  width: 620px;
  padding: 16px;
}

.etiqueta { border: 1px solid #ccc; padding: 8px 12px; }

A borda e o padding da etiqueta não são enfeite: são eles que dão largura ao texto e fazem as seis etiquetas passarem de 620 px. Sem essa regra, as seis cabem numa linha só e o exemplo não mostra nada.

Medindo a coordenada horizontal de cada etiqueta, agrupada por linha:

flex-wrap x de cada linha: [16, 174, 265, 402] [16, 141] grid 3 colunas x de cada linha: [16, 217, 419] [16, 217, 419]

No Flexbox, a primeira linha coube quatro etiquetas e a segunda coube duas, cada uma começando onde a anterior terminou. Nenhum x da linha de baixo bate com um x da linha de cima. No Grid, as duas linhas têm exatamente os mesmos três valores: 16, 217, 419. As colunas existem de verdade, e existem antes do conteúdo chegar.

Guarde essa pergunta, porque ela resolve 90% dos casos: o item da linha 2 precisa ficar embaixo do item da linha 1? Se precisa, é Grid. Se tanto faz, é Flexbox — e Flexbox vai dar menos trabalho.

Barra de navegação: o caso em que Flexbox ganha limpo

A barra do topo tem três partes: a marca à esquerda, o menu ao lado e o carrinho empurrado para a direita.

html
<header class="barra">
  <a class="marca" href="/">Livraria Bento</a>
  <nav class="menu">
    <a href="/lancamentos">Lançamentos</a>
    <a href="/clube">Clube</a>
    <a href="/eventos">Eventos</a>
  </nav>
  <a class="carrinho" href="/carrinho">Carrinho (2)</a>
</header>

Em Flexbox são três regras curtas, e nenhuma delas fala sobre quantidade:

css
.barra {
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 16px 24px;
}

.menu { display: flex; gap: 24px; }
.carrinho { margin-left: auto; }

Em Grid, a tradução ingênua — a que sai naturalmente de quem acabou de aprender grid-template-columns — declara as colunas:

css
.barra {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 32px;
  padding: 16px 24px;
}

.menu { display: grid; grid-template-columns: auto auto auto; gap: 24px; }

As duas versões ficam idênticas na tela. Até o dia em que a livraria abre a seção de sebo e alguém acrescenta um quarto link no HTML — uma linha, dentro do .menu, sem encostar no CSS:

diff
   <a href="/eventos">Eventos</a>
+  <a href="/sebo">Sebo</a>

Aí, numa janela de 1280 px:

flex barra 54.4px | 1o link x=154.7 | ultimo link right=459.8 | linhas: 1 grid 3 colunas barra 100.8px | 1o link x=154.7 | ultimo link right=497.5 | linhas: 2 grid auto-flow barra 54.4px | 1o link x=154.7 | ultimo link right=459.8 | linhas: 1

A barra em Grid quase dobrou de altura — de 54,4 px para 100,8 px — porque o quarto link não tinha coluna e caiu numa segunda linha. O Flexbox não mudou: display: flex não pergunta quantos filhos existem.

A terceira linha da medição é a versão de Grid que funciona:

css
.menu {
  display: grid;
  grid-auto-flow: column;
  justify-content: start;
  gap: 24px;
}

Ela dá o mesmo resultado do Flexbox, número por número. Repare no que foi preciso escrever para chegar lá: grid-auto-flow: column para dizer “vá para o lado” e justify-content: start para desligar o esticamento das trilhas auto. As duas coisas são o comportamento padrão do Flexbox. Quando você precisa de duas declarações para fazer o Grid imitar o padrão do Flexbox, o layout era de Flexbox. Se as propriedades de alinhamento ainda te confundem, elas estão destrinchadas em justify-content e align-items.

Galeria de cards: o caso em que Grid ganha limpo

Agora a vitrine, com sete livros e cards que devem se ajustar à largura da tela.

html
<section class="vitrine">
  <article class="livro"><h3>Grande Sertão: Veredas</h3><p>R$ 54,90</p></article>
  <article class="livro"><h3>Vidas Secas</h3><p>R$ 54,90</p></article>
  <!-- mais cinco -->
</section>

A versão em Flexbox é a receita que todo mundo escreve de cabeça:

css
.vitrine { display: flex; flex-wrap: wrap; gap: 24px; padding: 24px; }
.livro   { flex: 1 1 220px; border: 1px solid #ccc; padding: 16px; }

A versão em Grid delega o cálculo das colunas para o navegador:

css
.vitrine {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 24px;
  padding: 24px;
}

.livro { border: 1px solid #ccc; padding: 16px; }

Medindo a largura de cada card, agrupado por linha, em três larguras de tela:

1280px flex 5 card(s) de 227.2px | 2 card(s) de 604px 1280px grid 5 card(s) de 227.2px | 2 card(s) de 227.2px 768px flex 3 card(s) de 224px | 3 card(s) de 224px | 1 card(s) de 720px 768px grid 3 card(s) de 224px | 3 card(s) de 224px | 1 card(s) de 224px 420px flex 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px 420px grid 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px | 1 card(s) de 372px

Esse é o defeito mais conhecido do flex-wrap, e o nome que eu dou a ele aqui é linha órfã. Com sete cards em 1280 px, os cinco de cima ficam com 227,2 px e os dois que sobraram esticam para 604 px cada um — quase três vezes maiores que os irmãos. Em 768 px é pior: o último card sozinho vira uma faixa de 720 px.

Não é bug. É o flex-grow: 1 fazendo o trabalho dele, que é dividir a sobra entre os itens daquela linha. A linha de baixo tem menos itens, então cada um recebe uma fatia maior. Como cada linha é independente, o Flexbox não tem como saber que existia uma linha de 227,2 px acima. A mecânica dessa divisão está em flex-grow, flex-shrink e flex-basis.

As duas saídas de emergência que aparecem em fórum trocam só a linha do .livro, mantendo o resto da versão Flexbox:

css
/* saída 1: desligar o crescimento */
.livro { flex: 0 1 220px; border: 1px solid #ccc; padding: 16px; }

/* saída 2: fixar a coluna em um terço, descontando o gap */
.livro { flex: 0 0 calc(33.3333% - 16px); border: 1px solid #ccc; padding: 16px; }

Medi as duas com os mesmos sete cards, nas mesmas três larguras:

1280px flex 0 1 220px 5 de 220px | 2 de 220px | buraco à direita: 36px 1280px flex 0 0 calc(1/3) 3 de 394.7px | 3 de 394.7px | 1 de 394.7px | buraco à direita: 0px 768px flex 0 1 220px 3 de 220px | 3 de 220px | 1 de 220px | buraco à direita: 12px 768px flex 0 0 calc(1/3) 3 de 224px | 3 de 224px | 1 de 224px | buraco à direita: 0px 420px flex 0 1 220px 1 de 220px | 1 de 220px | 1 de 220px | 1 de 220px | 1 de 220px | 1 de 220px | 1 de 220px | buraco à direita: 152px 420px flex 0 0 calc(1/3) 3 de 108px | 2 de 123.4px/108px | 2 de 108px | buraco à direita: 0px

Desligar o crescimento com flex: 0 1 220px acaba com a linha órfã e cria outro defeito: os cards param de crescer e sobra um buraco à direita. Varrendo as larguras de 400 a 1400 px de dez em dez, 100 das 101 posições ficaram com mais de 1 px de sobra — em 420 px o buraco chega a 152 px.

flex: 0 0 calc(33.3333% - 16px) funciona: colunas iguais e zero sobra nas três larguras. O preço está na última linha da medição. O 33.3333% fixa o número de colunas em três, então em 420 px você recebe três colunas de 108 px em vez de uma coluna larga — e uma delas ainda estoura para 123,4 px, a de “Memórias Póstumas”, porque min-width: auto é o padrão do item flex e ele se recusa a encolher abaixo da palavra mais larga do título. Só um min-width: 0 explícito devolve os 108 px. Para o layout voltar a se adaptar, esse calc precisa de uma media query por faixa — que é exatamente o trabalho que o repeat(auto-fill, minmax(220px, 1fr)) faz sozinho, em uma linha.

Repare também que nas três larguras o Grid acertou sozinho, sem uma única media query. auto-fill conta quantas colunas de no mínimo 220 px cabem e distribui o resto com 1fr. O detalhe de quando usar auto-fill e quando usar auto-fit está em auto-fit e minmax() — a diferença aparece exatamente quando sobram colunas vazias.

Formulário com rótulo e campo: os dois resolvem, e um mente

O formulário do clube de assinatura tem três campos, com rótulos de tamanhos bem diferentes: “Nome”, “E-mail” e “Como conheceu a livraria”.

html
<form class="clube">
  <div class="linha"><label for="nome">Nome</label><input id="nome"></div>
  <div class="linha"><label for="email">E-mail</label><input id="email"></div>
  <div class="linha"><label for="origem">Como conheceu a livraria</label><input id="origem"></div>
</form>

A primeira versão em Flexbox é a que sai sem pensar: cada linha é um contêiner flex.

css
.clube { padding: 24px; max-width: 520px; }
.linha { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.linha input { flex: 1; }

A segunda é a correção que todo mundo aplica quando percebe o estrago: fixar a largura do rótulo.

css
.clube { padding: 24px; max-width: 520px; }
.linha { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.linha label { flex: 0 0 180px; }
.linha input { flex: 1; }

A versão em Grid não precisa de div nenhuma em volta — rótulo e campo são filhos diretos do formulário:

html
<form class="clube">
  <label for="nome">Nome</label><input id="nome">
  <label for="email">E-mail</label><input id="email">
  <label for="origem">Como conheceu a livraria</label><input id="origem">
</form>
css
.clube {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: center;
  gap: 12px;
  padding: 24px;
  max-width: 520px;
}

Medindo onde começa cada campo e qual a altura de cada rótulo:

flex ingênuo inputs começam em x = 78.7, 81.3, 217.4 → DESALINHADO | altura dos rótulos: 22.4, 22.4, 22.4 | form: 151.2px flex 180px inputs começam em x = 216, 216, 216 → ALINHADO | altura dos rótulos: 22.4, 22.4, 44.8 | form: 173.6px grid max-content inputs começam em x = 217.4, 217.4, 217.4 → ALINHADO | altura dos rótulos: 22.4, 22.4, 22.4 | form: 139.2px

A primeira versão é a mentira do título. Ela parece certa em qualquer captura de tela com rótulos de tamanho parecido, e desmonta na hora em que um rótulo é maior: os campos começam em 78,7, 81,3 e 217,4 px. Três posições diferentes, porque cada .linha é uma caixa flex separada e nenhuma delas sabe da largura do rótulo vizinho. É o mesmo motivo das etiquetas lá do começo.

A segunda alinha, mas ao preço de um número mágico. 180px é uma decisão que alguém tomou olhando para a tela naquele dia, e ela paga o preço: o rótulo longo não cabe em 180 px, quebra em duas linhas e sobe a altura do formulário de 151,2 px para 173,6 px.

O Grid chega ao mesmo alinhamento sem número nenhum. max-content na primeira coluna quer dizer “meça o rótulo mais largo e use esse valor” — por isso nenhum rótulo quebrou, os três ficaram em 22,4 px de altura e o formulário terminou com 139,2 px, o mais baixo dos três. E o que acontece quando o texto muda é a diferença que importa:

rótulos originais flex 180px x dos inputs: 216, 216, 216 | largura: 280, 280, 280 rótulos originais grid max-content x dos inputs: 217.4, 217.4, 217.4 | largura: 278.6, 278.6, 278.6 rótulos encurtados flex 180px x dos inputs: 216, 216, 216 | largura: 280, 280, 280 rótulos encurtados grid max-content x dos inputs: 88.5, 88.5, 88.5 | largura: 407.5, 407.5, 407.5

Trocando “Como conheceu a livraria” por “Origem”, o Grid remediu tudo e moveu os três campos juntos para 88,5 px, devolvendo 128,9 px de largura útil aos inputs. O Flexbox continuou em 216 px, com um vão de espaço em branco que ninguém pediu — até alguém lembrar de voltar no CSS e ajustar o 180px na mão.

Aqui está a página do catálogo: topo, filtros na lateral, conteúdo e rodapé colado no fim da tela mesmo quando há pouco conteúdo.

Em Grid, os quatro blocos são filhos diretos e o desenho vira texto:

html
<div class="pagina">
  <header>…</header>
  <aside>…</aside>
  <main>…</main>
  <footer>…</footer>
</div>
css
.pagina {
  display: grid;
  min-height: 100vh;
  gap: 16px;
  grid-template-columns: 260px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "topo    topo"
    "filtros conteudo"
    "rodape  rodape";
}

header { grid-area: topo; }
aside  { grid-area: filtros; }
main   { grid-area: conteudo; }
footer { grid-area: rodape; }

Em Flexbox, o mesmo layout exige um elemento a mais no HTML, porque aside e main precisam de um pai que os coloque lado a lado:

html
<div class="pagina">
  <header>…</header>
  <div class="miolo">
    <aside>…</aside>
    <main>…</main>
  </div>
  <footer>…</footer>
</div>
css
.pagina { display: flex; flex-direction: column; min-height: 100vh; gap: 16px; }
.miolo  { display: flex; gap: 16px; flex: 1; }
aside   { flex: 0 0 260px; }
main    { flex: 1; }

No celular, os filtros precisam descer para baixo do conteúdo. No Grid, a media query redesenha o mapa e mais nada:

css
@media (max-width: 760px) {
  .pagina {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr auto auto;
    grid-template-areas: "topo" "conteudo" "filtros" "rodape";
  }
}

No Flexbox, ela mexe em três seletores e usa order, uma numeração que você precisa manter de cabeça:

css
@media (max-width: 760px) {
  .miolo { flex-direction: column; }
  aside  { flex: 0 0 auto; order: 1; }
  main   { order: 0; }
}

Nos dois casos os quatro blocos levam border: 1px solid #ccc e padding: 12px, que é de onde vem o y=64 do miolo. Com as versões completas, medi geometria, ordem visual e ordem do Tab em 1280 px e em 420 px:

1280px · grid aside x=0 y=64 260x571 | main x=276 y=64 1004x571 | rodapé termina em 700px | 9 elementos ordem visual: link-topo → link-filtros → link-conteudo → link-rodape ordem do Tab: link-topo → link-filtros → link-conteudo → link-rodape 1280px · flex aside x=0 y=64 260x571 | main x=276 y=64 1004x571 | rodapé termina em 700px | 10 elementos ordem visual: link-topo → link-filtros → link-conteudo → link-rodape ordem do Tab: link-topo → link-filtros → link-conteudo → link-rodape 420px · grid aside x=0 y=587 420x48 | main x=0 y=64 420x507 | rodapé termina em 700px | 9 elementos ordem visual: link-topo → link-conteudo → link-filtros → link-rodape ordem do Tab: link-topo → link-filtros → link-conteudo → link-rodape 420px · flex aside x=0 y=587 420x48 | main x=0 y=64 420x507 | rodapé termina em 700px | 10 elementos ordem visual: link-topo → link-conteudo → link-filtros → link-rodape ordem do Tab: link-topo → link-filtros → link-conteudo → link-rodape

Empate técnico, pixel por pixel. A viewport tinha 700 px de altura e o rodapé termina exatamente em 700 px nos quatro casos — o rodapé grudado no fim sai de graça nas duas técnicas. A única diferença material é a div.miolo: 10 elementos contra 9.

O que não empata é a legibilidade das duas media queries que você acabou de ler. A do Grid é um desenho: três nomes empilhados, e dá para prever a tela sem abrir o navegador. A do Flexbox é um cálculo mental — order: 1 e order: 0 só fazem sentido se você lembrar de que existe uma div.miolo no meio do caminho. Em três meses, com mais dois blocos na página, essa numeração é onde o bug vai morar.

Escolha aqui o que a sua equipe vai conseguir ler. O passo a passo dessa sintaxe está em grid-template-areas, com as regras que fazem o desenho ser aceito ou recusado pelo navegador.

Quando os dois juntos é a resposta certa

O card de livro é o exemplo perfeito de por que a pergunta “Flexbox ou Grid?” costuma ter a resposta “sim”. O Grid põe os cards na malha; dentro de cada card, o botão precisa ficar colado embaixo, mesmo que o título ocupe duas linhas.

Agora o card tem três coisas dentro: título, autor e botão.

html
<section class="vitrine">
  <article class="livro">
    <h3>Torto Arado</h3><p class="autor">Itamar Vieira Junior</p><button>Comprar</button>
  </article>
  <!-- "Grande Sertão: Veredas" e "O Cortiço" -->
</section>

Só com Grid por fora, os cards ficam com a mesma altura — o stretch é o padrão — mas o botão de cada um para onde o texto terminou:

css
.vitrine { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; padding: 24px; }
.livro   { border: 1px solid #ccc; padding: 16px; }
h3, p    { margin: 0 0 8px; }

Bastam duas declarações a mais, agora dentro do card, para o botão descer:

css
.livro  { display: flex; flex-direction: column; }
.livro button { margin-top: auto; }

Com três livros de títulos diferentes, numa janela de 720 px, medindo o topo de cada botão:

só grid altura dos cartões: 147.2, 147.2, 147.2 | topo dos botões: 106.6, 132.8, 106.6 → em alturas diferentes grid + flex no cartão altura dos cartões: 145.8, 145.8, 145.8 | topo dos botões: 131.8, 131.8, 131.8 → na mesma linha

“Grande Sertão: Veredas” quebra o título em duas linhas e empurra o botão dele 26,2 px abaixo dos outros dois. Com o card virando uma coluna flex e margin-top: auto no botão, a margem automática come toda a sobra vertical e os três botões param em 131,8 px. Um contêiner grid por fora, um contêiner flex por dentro, cada um resolvendo o que sabe fazer.

O que muda no CSS gerado e no tamanho da folha

Passei as folhas de cada versão pelo css-tree para contar regras, declarações e bytes depois de minificar. Contei só o que faz layout — display, as trilhas, o alinhamento, gap, flex, order e as áreas —, deixando de fora padding, border e cor, que são iguais nas duas versões. Na barra, a versão de Grid contada é a que funciona, a do grid-auto-flow: column, não a que quebra a segunda linha:

caso regras declarações bytes minificados
barra · flexbox 3 6 103
barra · grid 2 8 156
vitrine · flexbox 2 4 68
vitrine · grid 1 3 89
formulário · flexbox 3 6 115
formulário · grid 1 4 86
página · flexbox 7 13 234
página · grid 6 13 427

Três leituras que valem mais que a tabela inteira.

A primeira: menos declarações não é o mesmo que menos bytes. A vitrine em Grid tem três declarações contra quatro do Flexbox, e mesmo assim é 21 bytes maior, porque repeat(auto-fill, minmax(220px, 1fr)) é um valor longo. Contar linhas de CSS não mede complexidade.

A segunda: na página inteira, as duas versões empatam em 13 declarações, e o Grid é quase o dobro em bytes — 427 contra 234. O peso extra é o desenho em grid-template-areas, que existe justamente para ser lido por gente. Você está pagando 193 bytes por documentação embutida no CSS, e é uma boa compra.

A terceira: os dois casos em que uma técnica ganha de fato — a barra para o Flexbox, o formulário para o Grid — ganham nas duas colunas ao mesmo tempo, menos declarações e menos bytes. Quando a escolha está certa, o CSS encolhe sozinho.

Performance: a medição que encerra a discussão

Ainda aparece em fórum a ideia de que “Grid é mais pesado”. Medi. Mesmo HTML, mesmo navegador, alternando só a classe do contêiner entre flex-wrap e grid auto-fill, forçando um recálculo de layout síncrono a cada mudança de largura. As duas técnicas convivem na mesma folha, e o que muda é uma classe:

css
.vitrine { padding: 24px; }
.flex        { display: flex; flex-wrap: wrap; gap: 24px; }
.flex .livro { flex: 1 1 220px; }
.grade       { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 24px; }
.livro       { border: 1px solid #ccc; padding: 16px; }
js
const alvo = document.getElementById('vitrine');
const tempos = [];

alvo.className = 'vitrine flex'; // ou 'vitrine grade'
alvo.offsetHeight;               // primeiro layout, fora da conta

for (let i = 0; i < 220; i += 1) {
  alvo.style.width = `${900 + (i % 40) * 5}px`;
  const t0 = performance.now();
  alvo.offsetHeight; // força o layout síncrono
  tempos.push(performance.now() - t0);
}

const uteis = tempos.slice(20).sort((a, b) => a - b); // descarta o aquecimento
const mediana = uteis[Math.floor(uteis.length * 0.5)];
const p90 = uteis[Math.floor(uteis.length * 0.9)];

O alvo é uma <section class="vitrine flex" id="vitrine"> com N cópias do mesmo card dentro. Foram 220 medições por técnica, com as 20 primeiras descartadas como aquecimento, em Chromium 151 headless num MacBook. A coluna p90 é o tempo abaixo do qual ficaram 90% das medições — é ela que mostra o pior caso corriqueiro:

cartões técnica mediana p90 amostras 24 flex-wrap 0.1 ms 0.2 ms 200 24 grid auto-fill 0.1 ms 0.2 ms 200 200 flex-wrap 0.7 ms 0.8 ms 200 200 grid auto-fill 0.8 ms 0.9 ms 200 2000 flex-wrap 7.5 ms 10.6 ms 200 2000 grid auto-fill 7.8 ms 11.4 ms 200

Numa vitrine real, com 24 cards, as duas medem 0,1 ms — abaixo da resolução que importa. Com 2000 cards, uma página que ninguém deveria estar montando assim, o Grid ficou 0,3 ms atrás. Repetindo a bateria inteira mais quatro vezes na mesma máquina, a mediana de 2000 cards ficou entre 7,5 e 8,1 ms no Flexbox e entre 7,8 e 8,4 ms no Grid: a oscilação de uma mesma técnica entre execuções é da mesma ordem de grandeza da diferença entre as duas. Nas faixas de 24 e 200 cards, as cinco execuções deram exatamente a mesma mediana.

Conclusão honesta: performance não é critério aqui. Escolha pela clareza do código, porque é isso que você vai pagar todo mês.

A tabela de decisão que eu uso

Depois de montar os quatro layouts nas duas técnicas, é isto que sobra como regra prática:

o que você está montando escolha por quê
barra, menu, grupo de botões Flexbox quantidade de itens muda sem tocar no CSS
miolo de um card, item de lista Flexbox é uma sequência num eixo só, com margin-top: auto de brinde
vitrine de produtos, galeria Grid colunas iguais em todas as linhas, sem linha órfã
formulário rótulo + campo Grid max-content alinha sem número mágico
esqueleto da página Grid grid-template-areas documenta o layout
centralizar uma coisa só qualquer um place-items: center funciona nos dois
sobreposição de camadas Grid dois filhos na mesma célula se empilham

E três perguntas, na ordem, para quando a tabela não decidir:

  1. As colunas precisam se alinhar entre linhas diferentes? Se sim, Grid. Foi o que separou a vitrine da barra.
  2. A quantidade de itens muda em tempo de execução? Se sim, Flexbox — ou Grid com auto-fill, que é a única forma de Grid que não pede o número.
  3. Você está prestes a digitar uma largura em pixel só para alinhar? Então o caso é de Grid, e a largura vira uma trilha max-content ou auto.

Existe ainda um quarto sinal, mais grosseiro e bem confiável: se você precisou criar uma div só para agrupar dois elementos, provavelmente era Grid. Se você precisou desligar duas propriedades do Grid para ele parar de esticar as coisas, era Flexbox.

Onde treinar isso ainda hoje

Pegue uma página que você já escreveu e refaça a vitrine dela com repeat(auto-fill, minmax(220px, 1fr)), medindo a largura dos cards da última linha antes e depois no DevTools. É o exercício mais curto que mostra o defeito da linha órfã com os seus próprios números.

Se faltar base em alguma das duas técnicas, elas têm lição própria: Flexbox no CSS para o eixo e o alinhamento, e CSS Grid para trilhas, fr e posicionamento. O guia de CSS mostra onde as duas entram na ordem de estudo e o que vem antes delas.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • css
  • flexbox
  • grid
  • layout
  • responsividade

Perguntas frequentes

Grid substituiu o Flexbox?
Não. São dois módulos separados da especificação, mantidos em paralelo, e resolvem problemas diferentes. Grid distribui itens numa malha definida pelo pai; Flexbox distribui itens ao longo de um eixo, deixando o tamanho de cada um influenciar o resultado. Página moderna usa os dois.
Dá para usar Grid dentro de Flexbox e vice-versa?
Dá, e é o normal. Um item flex pode ser um contêiner grid, e um item grid pode ser um contêiner flex. Cada elemento decide só como organiza os próprios filhos — o pai não interfere nisso.
Qual dos dois tem melhor suporte nos navegadores?
Os dois têm suporte universal nos navegadores em uso hoje. Flexbox estabilizou por volta de 2015 e Grid em 2017. Prefixo de fabricante e polyfill não fazem mais parte da discussão em projeto novo.
Preciso decorar todas as propriedades dos dois?
Não. Na prática, cinco propriedades resolvem quase tudo em Flexbox (display, gap, justify-content, align-items e flex) e quatro em Grid (display, gap, grid-template-columns e grid-template-areas). O resto você procura na hora em que aparecer o caso.
Por que meu grid-template-areas não funciona?
Quase sempre porque o desenho não é retangular ou porque as linhas têm quantidades diferentes de células. Cada área precisa formar um retângulo cheio, e todas as strings precisam ter o mesmo número de nomes. Um ponto isolado marca célula vazia.

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 (Playwright) + Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Relação do grid layout com outros métodos de layout — developer.mozilla.org
  2. CSS Flexible Box Layout Module Level 1 (W3C) — w3.org
  3. CSS Grid Layout Module Level 2 (W3C) — w3.org
  4. MDN — Basic concepts of flexbox — developer.mozilla.org

Continue por aqui