auto-fit e minmax(): grid responsivo sem media query
A linha de CSS que faz os cards se reorganizarem sozinhos em qualquer largura, e a diferença entre auto-fit e auto-fill que muda o resultado final.
repeat(auto-fit, minmax(240px, 1fr)) é a linha que faz uma galeria de cards
decidir sozinha quantas colunas cabem, do celular ao monitor grande, sem uma
única media query. O navegador conta quantas trilhas de 240px cabem no espaço
disponível e reparte a sobra entre elas.
Todos os exemplos aqui são o catálogo de uma livraria, a Página Sete: uma estante de cards, cada um com um livro. É o mesmo elemento que você montou na lição de CSS Grid, agora sem número fixo de colunas.
Duas palavras novas aparecem daqui em diante. Trilha (ou track) é cada faixa vertical do grid — o que a gente chama de coluna. Repetição automática é quando você não diz quantas trilhas quer, e deixa o navegador contar.
A linha única que resolve a estante da livraria
O markup é uma lista de cards, sem nenhuma classe de layout:
<main class="estante">
<article class="livro">Dom Casmurro</article>
<article class="livro">Grande Sertão</article>
<article class="livro">Vidas Secas</article>
<article class="livro">Capitães da Areia</article>
<article class="livro">A Hora da Estrela</article>
<article class="livro">Memórias Póstumas</article>
</main>O CSS do layout são quatro linhas. Junto com elas vai a regra de aparência do
card, que vale em todas as medições desta lição — guarde os 12px de
padding dela, porque é esse número que aparece nas alturas medidas mais
adiante:
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
padding: 16px;
}
.livro { background: #eee; padding: 12px; }Para conferir o que isso produz de verdade, medi a mesma página em quatro
larguras com um navegador de verdade, lendo o grid-template-columns
computado e o retângulo do primeiro card:
import { chromium } from 'playwright-core';
const livros = ['Dom Casmurro', 'Grande Sertão', 'Vidas Secas',
'Capitães da Areia', 'A Hora da Estrela', 'Memórias Póstumas'];
const html = `<main class="estante">${livros
.map((t) => `<article class="livro">${t}</article>`)
.join('')}</main>`;
const css = `
* { box-sizing: border-box; }
body { margin: 0; font: 16px/1.4 system-ui, sans-serif; }
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
padding: 16px;
}
.livro { background: #eee; padding: 12px; }
`;
const navegador = await chromium.launch();
for (const largura of [360, 768, 1024, 1440]) {
const pagina = await navegador.newPage({ viewport: { width: largura, height: 900 } });
await pagina.setContent(`<!doctype html><meta charset="utf-8"><style>${css}</style>${html}`);
const { colunas, card } = await pagina.evaluate(() => {
const trilhas = getComputedStyle(document.querySelector('.estante')).gridTemplateColumns;
return {
colunas: trilhas.split(' ').length,
card: document.querySelector('.livro').getBoundingClientRect().width,
};
});
console.log(`viewport ${String(largura).padStart(4)}px | ${colunas} coluna(s) | card de ${card}px`);
await pagina.close();
}
await navegador.close();Repare numa coisa contraintuitiva: o card não encolhe de forma contínua. Ele tem 360px no tablet e 320px no notebook — fica menor numa tela maior. É assim mesmo: cada vez que cabe mais uma coluna, a largura despenca e volta a crescer até caber a próxima. O gráfico dessa largura é um dente de serra, não uma rampa.
minmax(): o piso e o teto de cada trilha
minmax(240px, 1fr) diz duas coisas ao navegador: nenhuma trilha fica abaixo
de 240px, e nenhuma trilha para de crescer antes de a sobra acabar. Tirar o
minmax muda tudo. Compare as três variações a 1024px de largura, onde a
estante tem 992px de espaço útil:
/* 1 — trilha fixa, sem minmax */
.estante { grid-template-columns: repeat(auto-fit, 240px); }
/* 2 — piso 240px, teto elástico */
.estante { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
/* 3 — piso 240px, teto de 320px */
.estante { grid-template-columns: repeat(auto-fit, minmax(240px, 320px)); }repeat(auto-fit, minmax(240px, 1fr)) trilhas: 320px 320px 320px card: 320px | ocupa 992.00px de 992px disponiveis
repeat(auto-fit, minmax(240px, 320px)) trilhas: 320px 320px 320px card: 320px | ocupa 992.00px de 992px disponiveis
A contagem de colunas é a mesma nos três casos: três. O que muda é o que
acontece com a sobra. Com trilha fixa, os cards param em 240px e ficam 240px
mortos à direita (752 dos 992 disponíveis). É o 1fr no teto que transforma
essa sobra em largura de card, fechando os 992px. Sem ele, você tem repetição
automática sem elasticidade.
Aqui o teto de 320px não muda nada, porque 320px é justamente o que cabe. Ele só entra em ação quando existe sobra demais — e é exatamente o freio que segura o card gigante que aparece logo adiante.
A conta que decide quantas colunas cabem
O navegador não adivinha: ele resolve uma divisão. Sendo C a largura
disponível dentro da estante, min o piso do minmax e g o gap, o número
de trilhas é a parte inteira de (C + g) / (min + g), com mínimo de 1. O + g
existe porque a última coluna não leva gap depois dela.
Rodei a estante em sete larguras e comparei a previsão dessa fórmula com o que o Chromium realmente montou, com piso de 240px e gap de 16px:
.estante {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
padding: 16px;
}Sete acertos em sete. Guarde a consequência prática: cada coluna extra custa
min + gap, não min. Com piso de 240px e gap de 16px, o custo é 256px por
coluna. Se você aumentar o gap para 32px sem mexer no piso, o custo vira 272px
e uma coluna some antes do que você esperava.
Tem uma exceção que confunde muita gente: quando o teto do minmax é um
comprimento fixo, é ele — e não o piso — que entra na conta. Com
minmax(240px, 320px) numa janela de 1440px, o navegador criou quatro
trilhas, e não cinco: (1408 + 16) / (320 + 16) dá 4,23.
auto-fill cria trilhas vazias; auto-fit as colapsa
Agora a parte que separa as duas palavras. Ambas fazem a mesma coisa até aqui: calculam quantas trilhas cabem. A diferença aparece quando você tem menos cards do que trilhas.
auto-fillmantém as trilhas vazias ocupando espaço.auto-fitcolapsa as trilhas vazias para zero, e o1frreparte tudo entre as que sobraram.
/* a estante enche a fila com trilhas, mesmo sem card para elas */
.estante { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
/* a estante joga fora as trilhas sem card e estica as restantes */
.estante { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }Medindo a mesma janela de 1440px com seis, três e dois livros no catálogo:
estante com 3 livros, viewport de 1440px auto-fit | trilhas: 458.656px 458.672px 458.656px 0px 0px | card de 458.66px, primeira fila vai ate x=1424 de 1424 auto-fill | trilhas: 268.797px 268.797px 268.797px 268.797px 268.797px | card de 268.8px, primeira fila vai ate x=854 de 1424
estante com 2 livros, viewport de 1440px auto-fit | trilhas: 696px 696px 0px 0px 0px | card de 696px, primeira fila vai ate x=1424 de 1424 auto-fill | trilhas: 268.797px 268.797px 268.797px 268.797px 268.797px | card de 268.8px, primeira fila vai ate x=570 de 1424
Olhe as trilhas cruas com dois livros. O auto-fit devolveu
696px 696px 0px 0px 0px: as três trilhas que sobraram viraram zero, e os dois
cards ficaram com 696px cada. A conta bate: (1408 - 16) / 2 = 696, porque o
gap entre os dois sobreviventes continua lá. O auto-fill manteve as cinco
trilhas de 268,8px, e a fila para em x=570 numa área que vai até x=1424:
são 854px de estante vazia à direita.
Onde os dois dão exatamente o mesmo resultado
Volte ao primeiro bloco da saída anterior. Com seis livros, auto-fit e
auto-fill produziram trilhas idênticas, até a terceira casa decimal. E não é
sorte: com seis cards e cinco trilhas, não existe trilha vazia para colapsar.
Essa é a regra inteira, em uma frase: as duas palavras só divergem quando o
número de cards é menor que o número de trilhas que cabem. Numa listagem
paginada de 12 ou 24 produtos, que quase sempre enche a fila, trocar uma pela
outra não muda um pixel — e é por isso que tanta gente usa auto-fit a vida
inteira sem nunca ver a diferença.
A escolha, então, é sobre o caso magro. Se o catálogo da livraria filtrar por
“poesia contemporânea” e sobrarem dois títulos, você prefere dois cards de
696px atravessando a tela, ou dois cards no tamanho de sempre com espaço vazio
ao lado? auto-fit responde a primeira; auto-fill, a segunda. Não existe
resposta certa fora do desenho.
min() dentro do minmax para não estourar no celular
Aqui mora o bug mais chato desse layout. O piso do minmax é uma promessa
dura: se a trilha não couber, ela transborda em vez de encolher. Imagine
que o time definiu 360px como largura mínima do card:
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 16px;
padding: 24px;
}Num celular de 360px, a estante tem 312px de espaço útil depois do padding.
A trilha insiste nos 360px, o card vaza 24px para fora da tela e o
scrollWidth do documento vira 384 contra os 360 da janela: barra de
rolagem horizontal, o defeito que todo mundo culpa a imagem grande e quase
nunca é a imagem.
A correção cabe numa função. min(100%, 360px) devolve o menor dos dois: 360px
quando há espaço, e a largura do contêiner quando não há.
.estante {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
}No celular a trilha virou 312px e a rolagem sumiu. Em 1024px nada mudou: as
mesmas duas colunas de 480px. É por isso que vale escrever min() desde o
primeiro dia, mesmo com piso pequeno — ele não custa nada quando não é
necessário.
O erro mais comum: a declaração que o navegador descarta
Quem está aprendendo tenta a forma “óbvia” e cai num silêncio estranho: o layout vira uma coluna gigante e o DevTools não acusa erro nenhum. É porque a declaração inteira foi descartada na análise do CSS — valor inválido não avisa, some.
Testei seis valores pedindo ao próprio navegador que os atribuísse a
element.style.gridTemplateColumns e conferindo se a propriedade ficou vazia.
O elemento nem precisa estar na página: a análise do valor acontece na hora da
atribuição, então isto roda no console de qualquer aba aberta:
const valores = [
'repeat(auto-fit, 1fr)',
'repeat(auto-fit, minmax(240px, 1fr))',
'repeat(auto-fit, minmax(1fr, 240px))',
'repeat(auto-fit, minmax(auto, 1fr))',
'repeat(auto-fit, minmax(min(100%, 240px), 1fr))',
'repeat(auto-fill, minmax(15rem, 1fr))',
];
const el = document.createElement('div');
for (const valor of valores) {
el.style.gridTemplateColumns = '';
el.style.gridTemplateColumns = valor;
const aceito = el.style.gridTemplateColumns !== '';
console.log(`${aceito ? ' aceito ' : 'REJEITADO'} ${valor}`);
}Chromium HeadlessChrome/151.0.7922.34
Três padrões nessa lista, e uma regra só por trás deles: numa repetição
automática, o navegador precisa de um tamanho definido para conseguir contar
as trilhas. Por isso 1fr sozinho não serve, 1fr como piso não serve, e
auto como piso também não. minmax(240px, 1fr) funciona porque o piso é um
número que dá para dividir.
Com a declaração descartada, o grid-template-columns fica valendo none e o
grid cai numa coluna implícita ocupando os 992px inteiros — o “por que ficou
tudo empilhado” que aparece toda semana em comunidade de CSS.
grid-auto-flow: dense e os buracos que ele preenche
Livraria costuma ter card de destaque, mais largo que os outros. A estante continua com os mesmos seis livros; só o terceiro ganha uma classe a mais:
<main class="estante">
<article class="livro">Dom Casmurro</article>
<article class="livro">Grande Sertão</article>
<article class="livro destaque">Vidas Secas</article>
<article class="livro">Capitães da Areia</article>
<article class="livro">A Hora da Estrela</article>
<article class="livro">Memórias Póstumas</article>
</main>.destaque { grid-column: span 2; }Assim que um card ocupa duas trilhas, aparece buraco. Com três colunas, o
destaque em terceiro lugar não cabe na única trilha que restou da primeira
fila e desce inteiro, deixando um vão. Medi a posição de cada card, a 1024px,
antes e depois de ligar o dense:
.estante { grid-auto-flow: row; } /* padrão */
.estante { grid-auto-flow: row dense; } /* backfill */grid-auto-flow: row dense Dom Casmurro x= 16 y= 16 w= 320 Grande Sertão x= 352 y= 16 w= 320 Vidas Secas x= 16 y= 78.39 w= 656 Capitães da Areia x= 688 y= 16 w= 320 A Hora da Estrela x= 688 y= 78.39 w= 320 Memórias Póstumas x= 16 y=140.78 w= 320
Compare a linha de “Capitães da Areia”: sem dense, ele fica em y=78.39,
na segunda fila. Com dense, sobe para y=16 e tapa o vão da primeira. Todo
o resto se reacomoda atrás dele — “Memórias Póstumas”, que estava em x=352,
volta para x=16. O buraco sumiu e a estante ficou compacta.
Alinhar as trilhas: justify-content e place-items
Quando o teto do minmax é fixo, sobra espaço na estante — e aí justify-content
decide o que fazer com essa sobra. Com dois livros e trilhas travadas em 320px
numa janela de 1440px:
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 320px));
gap: 16px;
padding: 16px;
justify-content: center; /* ou space-between, start, end */
}Repare que a largura do card nunca muda: 320px nos três casos. justify-content
move as trilhas dentro do contêiner, não o conteúdo dentro das trilhas.
Quem cuida do conteúdo dentro da trilha é place-items, atalho de
align-items mais justify-items. Para ver isso, os três cards precisam ter
alturas diferentes:
<main class="estante">
<article class="livro">Dom Casmurro<br>Machado de Assis<br>R$ 39,90</article>
<article class="livro">Vidas Secas</article>
<article class="livro">Grande Sertão: Veredas<br>João Guimarães Rosa</article>
</main>.estante { align-items: start; } /* cada card com a própria altura */
.estante { place-items: start center; } /* e sem esticar na horizontal */Na medição abaixo o rótulo de cada linha são os 22 primeiros caracteres do
texto do card, sem as quebras de linha — por isso o primeiro aparece grudado
como Dom CasmurroMachado de.
O padrão stretch é o que dá aos cards a mesma altura de graça — os três com
91,17px, ditados pelo mais alto. Com align-items: start, cada um encolhe para
a própria altura (46,39px e 68,78px) e a estante fica desalinhada embaixo. E
com place-items: start center os cards param de esticar na horizontal
também, virando três larguras diferentes dentro de trilhas iguais.
O limite: quando você ainda precisa de media query
auto-fit responde a uma pergunta só — “quantas trilhas de X cabem aqui?”.
Tudo o que não é essa pergunta continua sendo trabalho de
media query. O caso típico: o desenho pede
exatamente quatro colunas no monitor grande, com respiro maior. A repetição
automática entrega cinco.
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
padding: 16px;
}
@media (min-width: 1200px) {
.estante {
grid-template-columns: repeat(4, 1fr);
gap: 32px;
padding: 48px;
}
}auto-fit + media query 768px -> 2 colunas de 360px 1440px -> 4 colunas de 312px
Nada impede as duas coisas de conviverem: auto-fit cuida da faixa larga de
telas intermediárias e a media query assume onde o desenho manda. Gap,
padding, tamanho de fonte e proporção de imagem nunca vão sair de um
minmax.
Uma última coisa que costuma virar media query desnecessária: o grid já responde ao contêiner, não à janela. Coloquei a mesma estante duas vezes na mesma página de 1440px — uma numa lateral de 320px, outra na área de conteúdo de 1120px:
<div class="pagina">
<aside class="lateral">
<main class="estante"><!-- os seis livros --></main>
</aside>
<section class="conteudo">
<main class="estante"><!-- os mesmos seis livros --></main>
</section>
</div>.pagina { display: grid; grid-template-columns: 320px 1fr; }
.estante {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
padding: 16px;
}Media query nenhuma resolveria isso, porque min-width mede a janela, e a
janela é a mesma nos dois casos. Quando você precisa mudar mais do que a
contagem de colunas conforme o espaço do pai, o caminho são as
container queries.
O que treinar agora
Pegue uma listagem que você já tem com media query e troque as três regras de
grid-template-columns por uma linha só:
repeat(auto-fit, minmax(min(100%, 280px), 1fr)).
Depois abra o DevTools, arraste a janela devagar e anote em que
larguras a contagem de colunas muda — confira com a fórmula
(C + gap) / (min + gap) desta lição.
Quando o layout tiver regiões nomeadas em vez de uma fila de cards iguais (cabeçalho, lateral, conteúdo), a ferramenta certa é grid-template-areas. O mapa completo da trilha está no guia de CSS, e a ordem de estudo, no índice da trilha de CSS.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
auto-fit ou auto-fill, qual eu uso por padrão?
Posso usar rem no lugar de px dentro do minmax?
Por que a contagem de colunas muda quando eu troco o gap?
O grid responde à largura da janela ou à do contêiner?
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 (headless, Playwright 1.62.1), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — CSS grid-template-columns — developer.mozilla.org
- MDN — minmax() — developer.mozilla.org
- CSS Grid Layout Module Level 2 — Repeat-to-fill: auto-fill and auto-fit — w3.org



