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

CSS Grid: grid-template-columns, rows e a unidade fr

Como montar uma grade de verdade: definir colunas e linhas, repartir o espaço com fr e posicionar um item pelo número da linha, sem contar no dedo.

Rodolfo Mori13 min de leitura

display: grid transforma um elemento em contêiner de grade. A partir daí, grid-template-columns diz quantas colunas existem e quanto cada uma mede, grid-template-rows faz o mesmo na vertical, e a unidade fr reparte o espaço que sobrou depois de descontar tamanhos fixos e gap.

Todos os exemplos desta lição montam a mesma peça: a grade de horários da Escola Girassol, uma escola de idiomas com cinco dias na horizontal e faixas de horário na vertical. E toda saída abaixo dos blocos é o que o motor de layout devolveu de verdade — cada exemplo foi carregado no Chromium 151 e medido com getComputedStyle(grade).gridTemplateColumns, que entrega as trilhas já resolvidas em pixels, e com getBoundingClientRect() em cada filho, que dá a posição e o tamanho final de cada célula.

Duas linhas de base valem para todas as medições, porque alguns números saem do conteúdo e não do template: a página usa box-sizing: border-box e o corpo está em font-size: 16px com line-height: 1.4, na fonte do sistema. Refazendo os exemplos com outra fonte, os tamanhos fixos batem igual e só os que dependem do texto mudam um pouco.

Linha, trilha e célula: o vocabulário do Grid

O Grid não trabalha com “colunas” no sentido de caixas. Ele trabalha com linhas de grade (as divisórias, numeradas a partir de 1), e o espaço entre duas divisórias vizinhas é uma trilha. O cruzamento de uma trilha de coluna com uma trilha de linha é uma célula.

1 2 3 4 -4 -3 -2 -1 célula linhas de coluna: positivas em cima, negativas embaixo 1 2 3 trilha

Três colunas produzem quatro divisórias. Essa diferença de um é o que faz gente contar errado quando vai posicionar um item.

O HTML da grade é uma lista plana de filhos. Não existe elemento de linha, não existe elemento de coluna: quem monta a tabela é o CSS.

html
<div class="grade">
  <div class="cab">Horário</div>
  <div class="cab">Segunda</div>
  <div class="cab">Terça</div>
  <div class="cab">Quarta</div>
  <div class="cab">Quinta</div>
  <div class="cab">Sexta</div>

  <div class="hora">08:00</div>
  <div class="aula">Inglês A1</div>
  <div class="aula">Espanhol A2</div>
  <div class="aula">Inglês A1</div>
  <div class="aula">Espanhol A2</div>
  <div class="aula">Conversação</div>
</div>
css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: 120px repeat(5, 1fr);
  grid-template-rows: 40px 64px;
  gap: 8px;
}

.cab,
.hora,
.aula {
  background: #eee;
  padding: 4px;
}
grid-template-columns: 120px 112px 112px 112px 112px 112px grid-template-rows: 40px 64px Horário x=0 y=0 largura=120 Segunda x=128 y=0 largura=112 Terça x=248 y=0 largura=112 Quarta x=368 y=0 largura=112 Quinta x=488 y=0 largura=112 Sexta x=608 y=0 largura=112 08:00 x=0 y=48 largura=120 Inglês A1 x=128 y=48 largura=112 Espanhol A2 x=248 y=48 largura=112 Inglês A1 x=368 y=48 largura=112 Espanhol A2 x=488 y=48 largura=112 Conversação x=608 y=48 largura=112

Doze div viraram seis colunas por duas linhas sem nenhuma marcação de linha no HTML. O navegador preenche as células na ordem em que os filhos aparecem: chegou ao fim da primeira linha, desce para a segunda.

Repare no 112px. Ninguém escreveu esse número: ele saiu de 1fr.

grid-template-columns e a unidade fr

fr é fração do espaço livre. E “livre” tem definição exata: é o que sobra depois de tirar as trilhas de tamanho fixo e todos os gap.

Na grade acima: 720px de contêiner, menos 120px da coluna de horário, menos cinco gap de 8px, dá 560px. Divididos por cinco colunas 1fr, 112px cada.

Com três colunas iguais e dois gap, a conta não fecha redonda — e aí aparece uma coisa que só o navegador mostra:

css
.grade { width: 720px; display: grid; gap: 8px; }

/* três variações do mesmo contêiner de 720px */
.grade { grid-template-columns: repeat(3, 1fr); }
.grade { grid-template-columns: 1fr 2fr 1fr; }
.grade { grid-template-columns: 2fr 1fr 1fr; }
repeat(3, 1fr) -> 234.656px 234.672px 234.672px (soma 704.000px) 1fr 2fr 1fr -> 176px 352px 176px (soma 704.000px) 2fr 1fr 1fr -> 352px 176px 176px (soma 704.000px)

Três leituras aí dentro.

A primeira: nas três variações a soma das trilhas dá 704px, que é 720 menos os dois gap de 8px. O gap é descontado antes da divisão, nunca depois.

A segunda: 234.656 e 234.672 são vizinhos, mas não são iguais. 704 dividido por 3 dá 234,6666… — um número que não existe na régua do navegador. O Chromium mede em frações de 1/64 de pixel (0,015625px), então ele corta cada trilha nesse múltiplo e devolve a sobra para as últimas: aqui as duas últimas ficaram com 1/64 a mais e a soma fechou os 704px exatos. Nem sempre fecha — no mesmo contêiner, só trocando o gap para 16px, as três trilhas saem iguais em 229.328px e um 1/64 de pixel simplesmente sobra. Você nunca vai enxergar essa diferença de 16 milésimos de pixel na tela; vai enxergá-la no DevTools, e agora sabe que não é bug.

A terceira: em 1fr 2fr 1fr, os 704px foram divididos por quatro frações (1 + 2 + 1), dando 176px por fração. A coluna do meio ficou com duas: 352px. O fr não é porcentagem — ele é peso.

E essa é justamente a diferença que quebra layout na vida real:

css
/* o mesmo contêiner de 720px, com gap: 8px */
.grade { grid-template-columns: repeat(3, 33.333%); }
.grade { grid-template-columns: repeat(3, 1fr); }
repeat(3, 33.333%) trilhas 239.984px 239.984px 239.984px conteúdo 736px em contêiner de 720px repeat(3, 1fr) trilhas 234.656px 234.672px 234.672px conteúdo 720px em contêiner de 720px

A porcentagem é calculada sobre a largura do contêiner e ignora o gap. Três trilhas de 239,98px mais dois gap de 8px somam 736px — 16px a mais do que cabe, que é exatamente o tamanho dos dois gap. Com fr, o conteúdo bate 720px na régua. Se você já teve uma grade que “vazava um tiquinho” para a direita, provavelmente era porcentagem com gap.

repeat(): doze colunas numa declaração

repeat(n, tamanho) escreve a mesma trilha n vezes. É a diferença entre uma linha de CSS e doze valores repetidos na mão.

css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 8px;
}
52.6562px 52.6719px 52.6719px 52.6562px 52.6719px 52.6719px 52.6562px 52.6719px 52.6719px 52.6562px 52.6719px 52.6719px 12 trilhas; soma 632.000px + 11 gaps de 8px = 720.000px

O valor computado devolve as doze trilhas expandidas — repeat() é açúcar de escrita, não uma estrutura nova. Doze colunas com onze gap de 8px consomem 88px, e os 632px restantes viram as trilhas. O padrão de arredondamento se repete de três em três, pelo mesmo motivo da seção anterior.

repeat() também aceita mistura: grid-template-columns: 120px repeat(5, 1fr) é exatamente o que a grade da escola usa.

Posicionar pelo número da linha

Até aqui todo item caiu na próxima célula livre. Para mandar um item para um lugar específico, você declara entre quais linhas de grade ele começa e termina — grid-column: início / fim, e o mesmo para grid-row.

A oficina de conversação ocupa segunda e terça. Segunda é a trilha 2; ela vive entre as linhas 2 e 3. Terça vive entre 3 e 4. As duas juntas, portanto: da linha 2 até a linha 4.

css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: 120px repeat(5, 1fr);
  grid-auto-rows: 64px;
  gap: 8px;
}

.oficina {
  grid-column: 2 / 4;
}
08:00 x=0 largura=120 Oficina de conversação x=128 largura=232 Inglês A1 x=368 largura=112 Espanhol A2 x=488 largura=112

232px é 112 + 8 + 112: as duas trilhas mais o gap que ficou no meio delas. Quando um item atravessa uma divisória, o gap daquela divisória vira área útil do item. E repare no Inglês A1: ele não foi declarado em lugar nenhum, mas começou em x=368, a quarta trilha — o posicionamento automático simplesmente pulou as células que a oficina já tinha reservado.

O erro clássico aqui é somar errado por um: para ocupar duas colunas a partir da segunda, muita gente escreve 2 / 3, que ocupa uma só. Conte divisórias, não caixas.

Contagem negativa: -1 é sempre a última

Números negativos contam do fim para o começo. A última divisória é -1, a penúltima é -2, e assim por diante. Isso serve para o caso mais comum de todos: uma faixa que atravessa a grade inteira.

css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: 120px repeat(5, 1fr);
  grid-auto-rows: 64px;
  gap: 8px;
}

.aviso {
  grid-column: 1 / -1;
}
Recesso na sexta-feira x=0 y=0 largura=720 08:00 x=0 y=72 largura=120 Inglês A1 x=128 y=72 largura=112 Espanhol A2 x=248 y=72 largura=112 Inglês A1 x=368 y=72 largura=112 Espanhol A2 x=488 y=72 largura=112 Conversação x=608 y=72 largura=112

O aviso mede 720px, a largura cheia do contêiner, e as aulas foram empurradas para a linha de baixo. O melhor de 1 / -1 é que ele continua certo quando a escola abrir aula no sábado e o template virar repeat(6, 1fr).

Só que existe uma armadilha, e ela pega gente experiente.

css
.grade {
  width: 720px;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 8px;
}

.aviso {
  grid-column: 1 / -1;
}
grid-template-columns: 174px 174px 174px 174px Recesso na sexta-feira x=0 largura=174 Inglês A1 x=182 largura=174 Espanhol A2 x=364 largura=174 Conversação x=546 largura=174

Quatro colunas de 174px na tela, e o aviso ficou com 174px: a largura de uma única trilha. Como não existe grid-template-columns declarado, o grid explícito tem zero coluna — as duas pontas do intervalo apontam para a mesma divisória e o 1 / -1 colapsa numa trilha. A correção é declarar as trilhas no template em vez de deixar o navegador criá-las.

span: ocupar duas trilhas sem saber onde o item está

span n diz “ocupe n trilhas a partir de onde você caiu”. É a forma de esticar um item sem fixar a posição dele.

O intensivo B2 dura duas horas, então ele atravessa duas faixas de horário:

css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: 120px repeat(3, 1fr);
  grid-auto-rows: 64px;
  gap: 8px;
}

.intensivo {
  grid-row: span 2;
}
08:00 x=0 y=0 altura=64 Inglês A1 x=128 y=0 altura=64 Intensivo B2 x=328 y=0 altura=136 Inglês A1 x=528 y=0 altura=64 09:00 x=0 y=72 altura=64 Espanhol A2 x=128 y=72 altura=64 Conversação x=528 y=72 altura=64

136px é 64 + 8 + 64 — de novo o gap virando área do item.

Agora olhe a segunda linha: “Conversação” pulou de x=328 para x=528. A célula da terceira coluna estava ocupada pelo intensivo que desceu, então o algoritmo de posicionamento automático seguiu para a próxima célula livre. Esse é o comportamento que faz uma grade “dançar” quando você adiciona um span: os itens de baixo se reorganizam sozinhos.

span e número podem se misturar: grid-column: 2 / span 3 começa na divisória 2 e ocupa três trilhas, e grid-column: span 3 / -1 termina no fim e ocupa três trilhas para trás.

Trilhas implícitas: grid-auto-rows e grid-auto-flow

Quando chegam mais itens do que o template previu, o navegador cria trilhas novas — as implícitas. E elas não obedecem ao grid-template-rows: por padrão têm tamanho auto, isto é, o do conteúdo.

Seis aulas num grid de três colunas com uma única linha declarada:

css
.grade {
  width: 720px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 64px;   /* uma linha só, e chegam seis aulas */
  gap: 8px;
}

/* segunda variação: */
.grade { grid-auto-rows: 96px; }
sem grid-auto-rows grid-template-rows: 64px 30.3906px grid-auto-rows: 96px grid-template-rows: 64px 96px 96px 40px, 9 aulas grid-template-rows: 64px 96px 40px

Na primeira leitura, a linha implícita mediu 30.3906px — a altura exata do texto com o padding, não os 64px da linha declarada. É a origem daquela grade em que a primeira fileira tem uma altura e as outras têm outra.

grid-auto-rows resolve, e aceita mais de um valor: com 96px 40px e nove aulas, as duas linhas implícitas saíram 96px e 40px, alternando o padrão.

O irmão dele é grid-auto-flow, que decide em que direção as trilhas implícitas nascem:

css
.grade { grid-auto-flow: row; }     /* padrão */
.grade { grid-auto-flow: column; }
grid-auto-flow: row colunas: 234.656px 234.672px 234.672px linhas: 64px 64px Inglês A1 x=0 y=0 Espanhol A2 x=242.66 y=0 Francês B1 x=485.33 y=0 Alemão A1 x=0 y=72 grid-auto-flow: column colunas: 204.156px 204.156px 204.156px 83.5312px linhas: 64px Inglês A1 x=0 y=0 Espanhol A2 x=212.16 y=0 Francês B1 x=424.31 y=0 Alemão A1 x=636.47 y=0

Com row, a quarta aula desceu para uma linha nova. Com column, ela criou uma coluna nova — e essa coluna implícita saiu com 83.5312px, o tamanho do conteúdo, porque grid-auto-columns não foi declarado. As três colunas do template encolheram de 234px para 204px para dar lugar a ela. É a mesma armadilha da seção anterior vista por outro ângulo: o que o navegador cria sozinho não segue o seu template.

gap no Grid: a diferença que aparece na última linha

gap existe nos dois modelos e a sintaxe é idêntica — gap: 16px, ou row-gap e column-gap separados. A diferença não está no espaçamento em si: está em quem define a largura dos itens.

No Grid, o gap fica entre trilhas, e as trilhas são as mesmas em todas as linhas. No Flexbox não existem trilhas: cada linha distribui o próprio espaço, e os itens da última linha crescem para ocupar a sobra. Cinco aulas, três por linha, nos dois modelos:

css
.grade { width: 720px; gap: 16px; }

.grid { display: grid; grid-template-columns: repeat(3, 1fr); }

.flex { display: flex; flex-wrap: wrap; }
.flex .aula { flex: 1 1 200px; }
display: grid grid-template-columns: 229.328px 229.328px 229.328px Inglês A1 x=0 largura=229.33 Espanhol A2 x=245.33 largura=229.33 Francês B1 x=490.66 largura=229.33 Alemão A1 x=0 largura=229.33 Italiano A1 x=245.33 largura=229.33 display: flex Inglês A1 x=0 largura=229.33 Espanhol A2 x=245.33 largura=229.34 Francês B1 x=490.67 largura=229.33 Alemão A1 x=0 largura=352 Italiano A1 x=368 largura=352

As três primeiras aulas são idênticas nos dois. A quarta e a quinta é que separam os modelos: no Grid continuam com 229.33px, alinhadas às colunas de cima; no Flexbox esticaram para 352px cada, porque flex-grow repartiu entre as duas a sobra daquela linha. Quem já viu um card gigante sozinho na última fileira estava usando Flexbox onde queria Grid — e o remédio está em flex-grow, flex-shrink e flex-basis, ou na troca de modelo.

O erro mais comum: 1fr não é minmax(0, 1fr)

Este é o bug que mais aparece em grade pronta, e ele é traiçoeiro porque só se manifesta com certo conteúdo. 1fr é atalho para minmax(auto, 1fr). O mínimo auto significa “nunca menor que o conteúdo mínimo da célula” — e o conteúdo mínimo de um e-mail sem espaços é o e-mail inteiro.

A página de turmas mostra o contato de cada professora, num contêiner de 360px (a largura de um celular comum):

html
<div class="grade">
  <div class="turma">Inglês A1</div>
  <div class="turma">professora.mariana.albuquerque@escolagirassol.com.br</div>
  <div class="turma">Francês B1</div>
</div>
css
.grade {
  width: 360px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.turma { background: #eee; padding: 8px; }
repeat(3, 1fr) trilhas: 59.0625px 423.656px 73.7031px conteúdo: 572px | contêiner: 360px

As três colunas deveriam ser iguais. A do meio ficou com 423px e as vizinhas foram esmagadas a 59px e 73px, e no fim a grade toda mede 572px dentro de um contêiner de 360px — 212px de vazamento e barra de rolagem horizontal na página.

A correção tem duas partes, e quase todo tutorial só conta a primeira:

css
.grade {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.turma {
  overflow-wrap: anywhere;
}
repeat(3, minmax(0, 1fr)) trilhas: 114.656px 114.672px 114.656px conteúdo: 538px | contêiner: 360px

minmax(0, 1fr) + overflow-wrap: anywhere trilhas: 114.656px 114.672px 114.656px conteúdo: 360px | contêiner: 360px

minmax(0, 1fr) conserta as trilhas: as três voltam a 114px iguais. Mas o texto continua sem saber quebrar, e agora quem vaza é a célula — 538px de conteúdo. Só com overflow-wrap: anywhere (ou overflow: hidden na célula, se cortar for aceitável) o total volta aos 360px do contêiner.

Guarde os dois sintomas separados: trilha desigual é problema de minmax; trilha certa e conteúdo estourando é problema de quebra de texto.

Um diagnóstico de dez segundos

Quando a grade simplesmente não acontece, abra o console e leia o valor computado do contêiner. Ele responde na hora:

css
/* faltou o display: grid */
.grade {
  width: 720px;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
display: block grid-template-columns: repeat(3, 1fr) Inglês A1 x=0 largura=720 Espanhol A2 x=0 largura=720 Francês B1 x=0 largura=720

Aí está a assinatura do problema. Num grid de verdade, grid-template-columns computado sai em pixels, com as trilhas resolvidas. Aqui ele saiu como repeat(3, 1fr), literalmente o que você escreveu: o navegador guardou a declaração e nunca a usou, porque o elemento não é um contêiner de grade. As três aulas empilharam com 720px cada, como três div normais.

A regra prática: se o computado ainda mostra repeat(, 1fr ou qualquer coisa que não seja pixel, falta display: grid — ou você está lendo o elemento errado, o que acontece com frequência quando existe um div embrulhando a grade.

O que treinar agora

Monte a grade de horários da sua própria escola imaginária, com a coluna de horário fixa em 120px, os dias em 1fr, um aviso em 1 / -1 no topo e pelo menos uma aula com grid-row: span 2. Depois estreite a janela e veja onde quebra.

Quando os números começarem a cansar, o próximo passo é dar nome às regiões em vez de contar divisórias: é o assunto de grid-template-areas. Para a grade se adaptar sozinha à largura disponível, sem uma única media query, o caminho é auto-fit com minmax(). E se ainda restar a dúvida de qual modelo usar em cada bloco da página, a comparação entre Flexbox e Grid resolve caso a caso. O mapa completo está no guia de CSS e na 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
  • grid-template-columns
  • fr
  • layout

Perguntas frequentes

A unidade fr funciona em grid-template-rows?
Funciona, mas o resultado depende da altura do contêiner. Com uma altura definida, as linhas repartem essa altura. Com altura automática, o fr ainda mantém a proporção, só que calculada a partir do conteúdo — medindo 1fr 2fr num contêiner sem altura o Chromium devolveu 38.3906px e 76.7812px; com height 300px, 97.3281px e 194.672px.
Dá para dar nome às linhas em vez de contar números?
Dá, e é o que salva grades grandes. Escreva o nome entre colchetes no template — grid-template-columns [borda] 120px [dias] repeat(5, 1fr) [fim] — e use o nome no item, com grid-column dias / fim. O resultado é idêntico ao dos números, sem quebrar quando você adiciona uma coluna.
O que acontece se eu escrever o fim antes do começo, como 4 / 2?
O navegador troca os dois de lugar e trata como 2 / 4. Num grid de quatro colunas de 174px com gap de 8px, o item declarado como 4 / 2 apareceu em x=182 com 356px de largura — exatamente as trilhas 2 e 3. Não é erro, mas confunde quem lê o CSS depois.
grid-gap ainda funciona?
Funciona, e em mais lugares do que o nome sugere: os navegadores mantêm grid-gap como apelido de gap em qualquer contexto, não só no Grid. No Chromium 151, grid-gap 24px num contêiner display flex computa gap 24px e separa os itens de verdade; em texto de duas colunas, grid-gap 40px computa column-gap 40px. Ainda assim escreva gap: grid-gap está marcado como obsoleto e o nome mente sobre onde ele age.

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

Fontes consultadas

  1. MDN — grid-template-columns — developer.mozilla.org
  2. CSS Grid Layout Module Level 2 — Flexible Lengths: the fr unit — w3.org
  3. MDN — Grid layout using line-based placement — developer.mozilla.org

Continue por aqui