Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

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

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.

Rodolfo Mori13 min de leitura

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:

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

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

js
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();
viewport 360px | 1 coluna(s) | card de 328px viewport 768px | 2 coluna(s) | card de 360px viewport 1024px | 3 coluna(s) | card de 320px viewport 1440px | 5 coluna(s) | card de 268.796875px

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:

css
/* 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, 240px) trilhas: 240px 240px 240px card: 240px | ocupa 752.00px de 992px disponiveis

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:

css
.estante {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
  padding: 16px;
}
viewport | conteudo | previsto | medido 320px | 288px | 1 | 1 ok 500px | 468px | 1 | 1 ok 700px | 668px | 2 | 2 ok 900px | 868px | 3 | 3 ok 1100px | 1068px | 4 | 4 ok 1300px | 1268px | 5 | 5 ok 1500px | 1468px | 5 | 5 ok

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-fill mantém as trilhas vazias ocupando espaço.
  • auto-fit colapsa as trilhas vazias para zero, e o 1fr reparte tudo entre as que sobraram.
css
/* 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 6 livros, viewport de 1440px auto-fit | trilhas: 268.797px 268.797px 268.797px 268.797px 268.797px | card de 268.8px, 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=1424 de 1424

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.

auto-fill · 5 trilhas de 268,8px, 3 vazias auto-fit · 3 trilhas colapsam, 2 cards de 696px Dom Casmurro Grande Sertão espaço vazio: 854px Dom Casmurro Grande Sertão

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:

css
.estante {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: 16px;
  padding: 24px;
}
repeat(auto-fit, minmax(360px, 1fr)) viewport 360px | trilhas: 360px | card w=360 | scrollWidth=384 clientWidth=360 | rolagem horizontal: SIM viewport 1024px | trilhas: 480px 480px | card w=480 | scrollWidth=1024 clientWidth=1024 | rolagem horizontal: nao

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

css
.estante {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
}
repeat(auto-fit, minmax(min(100%, 360px), 1fr)) viewport 360px | trilhas: 312px | card w=312 | scrollWidth=360 clientWidth=360 | rolagem horizontal: nao viewport 1024px | trilhas: 480px 480px | card w=480 | scrollWidth=1024 clientWidth=1024 | rolagem horizontal: nao

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:

js
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}`);
}
REJEITADO repeat(auto-fit, 1fr) aceito repeat(auto-fit, minmax(240px, 1fr)) REJEITADO repeat(auto-fit, minmax(1fr, 240px)) REJEITADO repeat(auto-fit, minmax(auto, 1fr)) aceito repeat(auto-fit, minmax(min(100%, 240px), 1fr)) aceito repeat(auto-fill, minmax(15rem, 1fr))

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:

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

css
.estante { grid-auto-flow: row; }        /* padrão */
.estante { grid-auto-flow: row dense; }  /* backfill */
grid-auto-flow: row 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= 78.39 w= 320 A Hora da Estrela x= 16 y=140.78 w= 320 Memórias Póstumas x= 352 y=140.78 w= 320

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:

css
.estante {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 320px));
  gap: 16px;
  padding: 16px;
  justify-content: center; /* ou space-between, start, end */
}
== justify-content, 2 livros, trilhas travadas em minmax(240px, 320px), viewport 1440px == sem justify-content | Dom Casmurro x=16 w=320 | Grande Sertão x=352 w=320 justify-content: center; | Dom Casmurro x=392 w=320 | Grande Sertão x=728 w=320 justify-content: space-between; | Dom Casmurro x=16 w=320 | Grande Sertão x=1104 w=320

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:

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

.estante { /* padrao: stretch stretch */ } Dom CasmurroMachado de x= 16 w= 320 h=91.17 Vidas Secas x= 352 w= 320 h=91.17 Grande Sertão: Veredas x= 688 w= 320 h=91.17 .estante { align-items: start; } Dom CasmurroMachado de x= 16 w= 320 h=91.17 Vidas Secas x= 352 w= 320 h=46.39 Grande Sertão: Veredas x= 688 w= 320 h=68.78 .estante { place-items: start center; } Dom CasmurroMachado de x= 98.09 w=155.81 h=91.17 Vidas Secas x=455.83 w=112.33 h=46.39 Grande Sertão: Veredas x=749.55 w=196.91 h=68.78

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.

css
.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;
  }
}
so auto-fit 768px -> 2 colunas de 360px 1440px -> 5 colunas de 268.8px

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:

html
<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>
css
.pagina { display: grid; grid-template-columns: 320px 1fr; }

.estante {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  padding: 16px;
}
na lateral de 320px -> 1 coluna(s): 288px no conteudo de 1120px -> 4 coluna(s): 260px 260px 260px 260px mesma regra CSS nos dois

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.

Ver todos os vídeos do canal
  • css
  • grid
  • auto-fit
  • minmax
  • responsivo

Perguntas frequentes

auto-fit ou auto-fill, qual eu uso por padrão?
Use auto-fill quando a largura do card faz parte do desenho e você não quer que ele estique com a estante vazia — catálogo com poucos itens, por exemplo. Use auto-fit quando ficar espaço sobrando à direita é pior que ter um card gigante.
Posso usar rem no lugar de px dentro do minmax?
Pode, e costuma ser melhor. minmax(15rem, 1fr) faz o piso da trilha acompanhar quem aumentou a fonte do navegador, então o card cresce junto com o texto em vez de espremê-lo.
Por que a contagem de colunas muda quando eu troco o gap?
Porque o gap entra na conta de quantas trilhas cabem. Com piso de 240px e gap de 16px, cada coluna extra custa 256px de largura; com gap de 32px, custa 272px, e uma coluna a menos aparece antes.
O grid responde à largura da janela ou à do contêiner?
À do contêiner. Se a estante estiver dentro de uma lateral de 320px numa página de 1440px, ela cria uma coluna só — o mesmo CSS produz layouts diferentes conforme o espaço que o pai deu.

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

Fontes consultadas

  1. MDN — CSS grid-template-columns — developer.mozilla.org
  2. MDN — minmax() — developer.mozilla.org
  3. CSS Grid Layout Module Level 2 — Repeat-to-fill: auto-fill and auto-fit — w3.org

Continue por aqui