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.
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:
* { 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.
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:
<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>.estantes {
display: flex;
flex-wrap: wrap;
gap: 16px;
width: 620px;
padding: 16px;
}
.etiqueta { border: 1px solid #ccc; padding: 8px 12px; }.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:
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.
<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:
.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:
.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:
<a href="/eventos">Eventos</a>
+ <a href="/sebo">Sebo</a>Aí, numa janela de 1280 px:
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:
.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.
<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:
.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:
.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:
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:
/* 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:
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.
Já 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”.
<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.
.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.
.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:
<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>.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:
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:
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.
Layout de página inteira com header, aside e footer
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:
<div class="pagina">
<header>…</header>
<aside>…</aside>
<main>…</main>
<footer>…</footer>
</div>.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:
<div class="pagina">
<header>…</header>
<div class="miolo">
<aside>…</aside>
<main>…</main>
</div>
<footer>…</footer>
</div>.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:
@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:
@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:
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.
<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:
.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:
.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:
“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:
.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; }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:
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:
- As colunas precisam se alinhar entre linhas diferentes? Se sim, Grid. Foi o que separou a vitrine da barra.
- 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. - 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-contentouauto.
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.
DevClub no YouTubeDominando Display Flex | Guia para iniciantesAssistir a aula
DevClub no YouTubeAprendendo CSS DISPLAY GRID com um ProjetoAssistir a aula
Perguntas frequentes
Grid substituiu o Flexbox?
Dá para usar Grid dentro de Flexbox e vice-versa?
Qual dos dois tem melhor suporte nos navegadores?
Preciso decorar todas as propriedades dos dois?
Por que meu grid-template-areas não funciona?
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 (Playwright) + Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Relação do grid layout com outros métodos de layout — developer.mozilla.org
- CSS Flexible Box Layout Module Level 1 (W3C) — w3.org
- CSS Grid Layout Module Level 2 (W3C) — w3.org
- MDN — Basic concepts of flexbox — developer.mozilla.org


