Tipografia no CSS: fonte, escala e clamp() fluido
Como escolher a pilha de fontes, acertar line-height e medida de linha, e montar uma escala tipográfica que cresce sozinha com clamp().
Tipografia no CSS é um punhado de decisões pequenas: qual pilha de fontes, como
a fonte chega até o leitor, quanto de line-height, quantos caracteres cabem
por linha e como o tamanho cresce junto com a tela. As quatro primeiras você
resolve uma vez por projeto. A última é onde clamp() entra.
Todos os exemplos são do catálogo online da Ponto de Fuga, uma livraria de bairro com onze mil títulos e um clube de leitura que se encontra na última sexta do mês. A página que vamos vestir é a resenha: um título, um parágrafo longo e uma chamada em corpo maior.
font-family: a pilha e o fallback que evita o pulo do texto
font-family não recebe uma fonte, recebe uma lista de candidatas. O
navegador anda por ela da esquerda para a direita e para na primeira que existe
na máquina de quem lê — ou que terminou de baixar.
Isso quer dizer que o fallback não é enfeite para o caso raro de a fonte falhar. Ele é o que a pessoa lê nos primeiros instantes de toda visita, enquanto o arquivo da fonte ainda está na rede. E fontes diferentes ocupam larguras bem diferentes.
Medi a mesma frase, a 18px, em seis pilhas:
.a { font-family: Georgia, serif; }
.b { font-family: Helvetica, Arial, sans-serif; }
.c { font-family: system-ui, sans-serif; }
.d { font-family: 'Times New Roman', serif; }
.e { font-family: Verdana, sans-serif; }
.f { font-family: ui-monospace, monospace; }A mesma frase, no mesmo tamanho, ocupa de 643px a 950px. Verdana é 16% mais
larga que Georgia; a monospace é 34% mais larga e já quebra numa linha a mais.
Repare que a última coluna também muda: line-height: normal vale 21px em
Georgia e 22px em Verdana, porque cada fonte carrega a própria altura de linha
sugerida.
Escolha o fallback pelo desenho e pela largura da fonte que você vai carregar. Para a Literata, que é uma serifada de leitura, Georgia é a vizinha mais próxima que existe em quase toda máquina:
:root {
--fonte-texto: Literata, Georgia, 'Times New Roman', serif;
--fonte-marca: 'Space Grotesk', system-ui, sans-serif;
}@font-face, font-display e o texto que some
Uma fonte própria entra pela regra @font-face, que dá um nome a um arquivo. O
font-display decide o que o navegador faz enquanto esse arquivo não chega:
@font-face {
font-family: 'Literata';
src: url('/literata.woff2') format('woff2');
font-display: block;
}
p {
font-family: 'Literata', Georgia, serif;
font-size: 18px;
line-height: 1.6;
width: 380px;
}Omitir a linha não é ficar neutro. O valor inicial da propriedade é auto, que
entrega a decisão ao navegador — e na prática os motores atuais resolvem auto
como block: por até três segundos o navegador reserva o espaço do texto e
não pinta nada. É o FOIT, flash of invisible text.
Para provar isso, servi a fonte com 1,5 segundo de atraso e capturei a tela aos
400 ms — antes de a fonte chegar — em cinco situações: sem declarar nada, com
auto, com block, com swap, e com o parágrafo escondido por
visibility: hidden para servir de gabarito de “página sem texto”.
sem font-display == escondido (FOIT)? true auto == escondido (FOIT)? true
Três das capturas são byte a byte idênticas à do gabarito: a de quem não
declarou nada, a de auto e a de block têm o mesmo tamanho e o mesmo hash da
página com o parágrafo escondido. Não havia texto nenhum na tela, só um retângulo
de espaço reservado. Com swap, o mesmo instante tem 19 kB de pixels — o
parágrafo já está lá, em Georgia.
O pulo: size-adjust faz o fallback ocupar o mesmo espaço
Trocar o FOIT pelo FOUT — flash of unstyled text, o texto que aparece no fallback e depois troca de fonte — resolve o texto invisível e cria outro problema: quando a Literata chega, ela ocupa mais espaço que a Georgia e o parágrafo empurra tudo que está abaixo. Medi a altura da caixa aos 400 ms e depois da troca.
A correção é declarar o fallback como uma @font-face própria, esticada até
casar com a largura da fonte real:
@font-face {
font-family: 'Georgia ajustada';
src: local('Georgia');
size-adjust: 105.38%;
}
p {
font-family: 'Literata', 'Georgia ajustada', serif;
}Com Georgia crua, o parágrafo salta de 4 para 5 linhas e desloca 28,79px de conteúdo — um layout shift visível. Com o fallback ajustado, o pulo é 0,00px: o texto já nasce com o número de linhas final.
Aqueles 105,38% não são chute. Medi a largura do parágrafo inteiro sem quebra nas duas fontes (1520,4px na Literata contra 1442,7px na Georgia) e dividi uma pela outra. A razão depende do texto — meça com o seu texto, não com uma palavra solta.
line-height sem unidade é o único que herda direito
line-height aceita número puro (1.6), comprimento (28.8px) e porcentagem
(160%). Parecem três jeitos de dizer a mesma coisa. Não são — e a diferença só
aparece quando um filho tem tamanho de fonte diferente do pai.
.resenha { font-size: 18px; line-height: 1.6; }
.resenha .chamada { font-size: 32px; }Rodei as quatro variantes de line-height na mesma estrutura, lendo o valor
computado no pai e no filho:
O número puro é o único que herda como fator: o filho de 32px recebe
51.2px, que é 32 × 1,6. Com 28.8px e com 160% o valor é calculado uma vez,
no pai, e o que desce para o filho é o resultado em pixels. A chamada de 32px
fica com linha de 28,8px — menor que a própria fonte. As linhas se encavalam.
Para o corpo de texto, 1.5 a 1.7. Para títulos grandes, algo entre 1.1 e
1.25: quanto maior a letra, menos respiro entre linhas ela precisa.
Medida de linha: 65ch não são 65 caracteres
A recomendação clássica é entre 45 e 75 caracteres por linha. Acima disso, o olho perde o começo da linha seguinte; abaixo, a leitura vira uma sequência de saltos.
A unidade ch parece feita para isso — ela vale a largura do caractere 0 na
fonte atual. O problema é que numa fonte proporcional o 0 é bem mais largo que
a letra média: em Georgia 18px ele mede 11,05px, contra 8,04px de largura média
por caractere no texto da resenha — 37% a mais. Por isso em 65ch cabe bem mais
que 65 letras. Medi contando de verdade, caractere por caractere, em qual linha
cada um caiu:
p {
font-size: 18px;
line-height: 1.6;
max-width: 65ch;
}Em Georgia, 65ch entrega 85 caracteres por linha — vinte a mais que o topo
da faixa recomendada. Só na monospace o número bate com a intuição, porque ali
todo caractere tem a largura do 0.
O medidor é um laço que pede o retângulo de cada caractere e agrupa pela coordenada vertical. Cole no console do DevTools, em qualquer página:
const texto =
'A Ponto de Fuga abriu em 2019 numa sala de esquina, com três estantes e ' +
'uma poltrona que ninguém queria comprar. Hoje são onze mil títulos, um ' +
'clube de leitura que se encontra toda última sexta e um catálogo online ' +
'que a Marta atualiza de manhã, antes de abrir a loja para a rua.';
const palco = document.body.appendChild(document.createElement('div'));
const medir = (ch) => {
palco.innerHTML = `<p style="font: 18px/1.6 Georgia, serif; max-width: ${ch}ch">${texto}</p>`;
const p = palco.querySelector('p');
const no = p.firstChild;
const range = document.createRange();
const porLinha = new Map();
for (let i = 0; i < texto.length; i++) {
range.setStart(no, i);
range.setEnd(no, i + 1);
const r = range.getBoundingClientRect();
if (r.width === 0 && r.height === 0) continue;
const chave = Math.round(r.top);
porLinha.set(chave, (porLinha.get(chave) ?? 0) + 1);
}
const cheias = [...porLinha.values()].slice(0, -1); // a última linha não conta
return {
ch,
px: +p.getBoundingClientRect().width.toFixed(1),
media: +(cheias.reduce((a, b) => a + b, 0) / cheias.length).toFixed(1),
};
};
console.log('Georgia 18px — quantos caracteres reais cabem em cada valor de ch');
for (let ch = 35; ch <= 70; ch += 5) {
const d = medir(ch);
const faixa = d.media >= 45 && d.media <= 75 ? ' (dentro dos 45 a 75)' : '';
console.log(
`max-width: ${String(d.ch).padStart(2)}ch = ${String(d.px).padStart(6)}px = ${d.media} caracteres${faixa}`,
);
}Para a resenha da Ponto de Fuga em Georgia, o alvo de 65 caracteres reais fica em
50ch, ou 552px. É esse valor que vai para o CSS — e ele muda se a fonte
mudar. Mais sobre ch e as outras unidades relativas na lição de
unidades no CSS.
clamp(): mínimo, preferido e máximo numa declaração
clamp(mínimo, preferido, máximo) devolve o preferido, desde que ele fique
entre os outros dois. Se passar do teto, devolve o teto. Se cair abaixo do piso,
devolve o piso. Uma declaração faz o trabalho de duas media queries e do
intervalo inteiro entre elas.
h2 {
font-size: clamp(1.5rem, 5vw, 3rem);
}A curva tem três trechos: um patamar embaixo, uma rampa no meio, um patamar em
cima. Repare em 480px, onde 5vw dá exatamente 24px: é ali que a rampa começa.
E em 960px, onde 5vw dá 48px: é ali que ela trava.
5vw é fácil de escrever e ruim de controlar: a rampa é íngreme demais e você
não escolhe onde ela começa nem onde termina. A seção seguinte resolve isso.
Montando a escala do corpo ao h1 com uma razão só
Uma escala tipográfica é uma razão aplicada repetidas vezes ao tamanho do corpo. Com razão 1,25 (a terça maior), o corpo de 16px gera 20px, 25px e 31,25px. Você escolhe dois números — o corpo na tela estreita e o corpo na tela larga — e a escala nasce dos dois.
Falta transformar cada par em um clamp. A conta é uma reta que passa por dois
pontos: (320px de janela, tamanho mínimo) e (1280px, tamanho máximo). A
inclinação vira vw; onde a reta corta o eixo vira rem.
const BASE_MIN = 16; // px do corpo na tela estreita
const BASE_MAX = 20; // px do corpo na tela larga
const RAZAO = 1.25; // uma razão só, dos dois lados
const VW_MIN = 320;
const VW_MAX = 1280;
const NIVEIS = ['corpo', 'h3', 'h2', 'h1'];
const arred = (n) => +n.toFixed(3);
function fluido(minPx, maxPx) {
const inclinacao = (maxPx - minPx) / (VW_MAX - VW_MIN);
const intercepto = minPx - inclinacao * VW_MIN;
return {
min: arred(minPx / 16),
rem: arred(intercepto / 16),
vw: arred(inclinacao * 100),
max: arred(maxPx / 16),
};
}
console.log(':root {');
NIVEIS.forEach((nome, i) => {
const minPx = BASE_MIN * RAZAO ** i;
const maxPx = BASE_MAX * RAZAO ** i;
const f = fluido(minPx, maxPx);
console.log(
` --fonte-${nome}: clamp(${f.min}rem, ${f.rem}rem + ${f.vw}vw, ${f.max}rem);` +
` /* ${arred(minPx)}px a ${arred(maxPx)}px */`,
);
});
console.log('}');Quatro variáveis, e cada elemento passa a apontar para uma delas. Se a razão
mudar, você roda o script de novo e cola o novo :root — nenhum seletor precisa
ser tocado. É o mesmo raciocínio das
variáveis CSS aplicado a tamanho de fonte.
Agora a pergunta que importa: o navegador entrega o que o script prometeu? Medi
o h1 de 320px a 1920px, de 80 em 80 pixels.
h1 {
font-size: clamp(1.953rem, 1.79rem + 0.814vw, 2.441rem);
}piso 31.248px, teto 39.056px primeira largura acima do piso: 321px (31.2529px) primeira largura já no teto: 1280px (39.0560px)
Uma varredura de 1 em 1 pixel achou as duas dobradiças: 320px é a última
largura ainda encostada no piso — em 321px a fonte já saiu dele — e 1280px é
exatamente a primeira largura que atinge o teto. São os dois números que
entraram no script como VW_MIN e VW_MAX. Arredondar os coeficientes para
três casas custou 0,007px no teto (39,056 em vez de 39,063). Ninguém enxerga
isso; a conta está certa.
O clamp que quebra o zoom, e a correção com rem no meio
Aqui mora o defeito mais comum de tipografia fluida, e ele não aparece em teste
nenhum de layout: clamp com só vw no meio ignora o zoom do navegador.
O motivo é geométrico. Zoom de 200% em uma janela de 1280 pixels de tela deixa a
viewport com 640 pixels de CSS, e cada pixel de CSS passa a ocupar dois pixels
de tela. Um valor em vw cai pela metade e é multiplicado por dois: dá no
mesmo. Um valor em rem não cai — e é ele que faz o texto crescer.
Medi três variantes do mesmo h1, sem e com zoom:
.so-vw-min-baixo { font-size: clamp(1rem, 3.051vw, 2.441rem); }
.so-vw-min-alto { font-size: clamp(1.953rem, 3.051vw, 2.441rem); }
.rem-mais-vw { font-size: clamp(1.953rem, 1.79rem + 0.814vw, 2.441rem); }variante | 100%: css / tela | 200%: css / tela | o texto cresceu ———————+—————––+—————––+–––––––– só vw, mínimo baixo | 39.05 / 39.05px | 19.53 / 39.05px | 1.00x só vw, mínimo alto | 39.05 / 39.05px | 31.25 / 62.5px | 1.60x rem + vw (a nossa) | 39.06 / 39.06px | 33.85 / 67.7px | 1.73x
A primeira variante cresce 1,00x: a pessoa apertou zoom duas vezes e o título ficou exatamente do mesmo tamanho na tela. Isso reprova no critério 1.4.4 da WCAG, que exige texto redimensionável até 200%.
A segunda melhora por acidente — o zoom empurra o vw para baixo do mínimo, e o
mínimo está em rem. Só a terceira tem rem no termo preferido, e é a única
que cresce em toda a faixa, não só nas pontas.
text-wrap: balance no título, pretty no parágrafo
Faltam as quebras. Um título de três linhas em que a última tem uma palavra solta é feio, e nenhuma escala conserta isso — quem decide onde quebrar é o algoritmo de linha do navegador.
text-wrap: balance manda o navegador distribuir os caracteres igualmente entre
as linhas. Ele custa caro, então só vale em blocos curtos: títulos, legendas,
chamadas.
body { font-family: Georgia, serif; }
h2 {
font-size: 28px;
line-height: 1.25;
width: 420px;
text-wrap: balance;
}
p {
font-size: 18px;
line-height: 1.6;
width: 360px;
text-wrap: pretty;
}No título, balance transformou 30/20/8 em 20/19/19 — três linhas do mesmo
tamanho, sem a sobra de oito caracteres. No parágrafo, pretty reorganizou as
quatro linhas para tirar a órfã de seis caracteres, deixando nove na última.
pretty foi feito para texto longo justamente porque não tenta equilibrar tudo:
ele mexe só nas últimas linhas. Onde não há órfã, ele não muda nada — foi o que
aconteceu quando testei o mesmo parágrafo numa caixa de 380px.
Os cinco defeitos, e o que os causa
| o que aparece na tela | causa | correção |
|---|---|---|
| o texto some por um segundo | font-display ausente: auto age como block |
font-display: swap |
| o conteúdo desce quando a fonte chega | métricas do fallback diferentes | size-adjust numa @font-face de fallback |
| linhas do título se encavalam | line-height com unidade, herdado em px |
line-height sem unidade |
| a linha atravessa a tela inteira | sem max-width no bloco de texto |
max-width em ch, medido na sua fonte |
| o título não cresce com o zoom | clamp só com vw no meio |
somar rem ao vw no termo preferido |
Juntando tudo, a folha de tipografia da Ponto de Fuga cabe em menos de trinta linhas — e cada valor dela apareceu numa medição desta página:
@font-face {
font-family: 'Literata';
src: url('/fonts/literata.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'Georgia ajustada';
src: local('Georgia');
size-adjust: 105.38%;
}
:root {
--fonte-texto: 'Literata', 'Georgia ajustada', serif;
--fonte-corpo: clamp(1rem, 0.917rem + 0.417vw, 1.25rem);
--fonte-h2: clamp(1.563rem, 1.432rem + 0.651vw, 1.953rem);
--fonte-h1: clamp(1.953rem, 1.79rem + 0.814vw, 2.441rem);
}
body {
font-family: var(--fonte-texto);
font-size: var(--fonte-corpo);
line-height: 1.6;
}
h1 { font-size: var(--fonte-h1); line-height: 1.15; text-wrap: balance; }
h2 { font-size: var(--fonte-h2); line-height: 1.2; text-wrap: balance; }
p { max-width: 50ch; text-wrap: pretty; }O que treinar agora
Pegue uma página que você já escreveu, abra o DevTools e faça três medições: o
line-height computado de um título grande, a largura em pixels do seu bloco de
texto e o font-size do h1 com zoom em 200%. Cada uma das três costuma
revelar um dos defeitos da tabela acima.
Com a fonte no lugar, sobra a fronteira entre tipografia e layout: a media query ainda decide o que muda por tamanho de tela, enquanto as container queries deixam o componente responder ao espaço que ele realmente recebeu — o que também vale para escolher o tamanho da fonte dentro de um card. Depois disso, a próxima lição da trilha põe esse texto em movimento, com transition e transform. O mapa completo da matéria está no guia de CSS.
Perguntas frequentes
Uso o Google Fonts pelo link ou hospedo a fonte no meu servidor?
Quantos pesos da mesma fonte vale a pena carregar?
Qual a diferença entre font-display swap e optional?
Posso usar clamp() no line-height e no espaçamento também?
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, macOS), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN - clamp() — developer.mozilla.org
- MDN - @font-face: font-display — developer.mozilla.org
- MDN - @font-face: size-adjust — developer.mozilla.org
- MDN - text-wrap — developer.mozilla.org
- WCAG 2.2 - Understanding Success Criterion 1.4.4: Resize Text — w3.org


