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.
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.
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.
<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>.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;
}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:
.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; }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:
/* o mesmo contêiner de 720px, com gap: 8px */
.grade { grid-template-columns: repeat(3, 33.333%); }
.grade { grid-template-columns: repeat(3, 1fr); }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.
.grade {
width: 720px;
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 8px;
}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.
.grade {
width: 720px;
display: grid;
grid-template-columns: 120px repeat(5, 1fr);
grid-auto-rows: 64px;
gap: 8px;
}
.oficina {
grid-column: 2 / 4;
}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.
.grade {
width: 720px;
display: grid;
grid-template-columns: 120px repeat(5, 1fr);
grid-auto-rows: 64px;
gap: 8px;
}
.aviso {
grid-column: 1 / -1;
}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.
.grade {
width: 720px;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 8px;
}
.aviso {
grid-column: 1 / -1;
}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:
.grade {
width: 720px;
display: grid;
grid-template-columns: 120px repeat(3, 1fr);
grid-auto-rows: 64px;
gap: 8px;
}
.intensivo {
grid-row: span 2;
}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:
.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; }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:
.grade { grid-auto-flow: row; } /* padrão */
.grade { grid-auto-flow: column; }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:
.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; }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):
<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>.grade {
width: 360px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.turma { background: #eee; padding: 8px; }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:
.grade {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.turma {
overflow-wrap: anywhere;
}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:
/* faltou o display: grid */
.grade {
width: 720px;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}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.
Perguntas frequentes
A unidade fr funciona em grid-template-rows?
Dá para dar nome às linhas em vez de contar números?
O que acontece se eu escrever o fim antes do começo, como 4 / 2?
grid-gap ainda 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.0.7922.34 (Playwright 1.62.1), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — grid-template-columns — developer.mozilla.org
- CSS Grid Layout Module Level 2 — Flexible Lengths: the fr unit — w3.org
- MDN — Grid layout using line-based placement — developer.mozilla.org



