Design

CSS clamp() e tipografia fluida: a matemática (e a armadilha de acessibilidade)

A maioria dos tutoriais trata `clamp()` como uma caixa-preta: você joga três valores e sai uma fonte que "cresce sozinha". Mas o valor do meio não é mágica, é a equação de uma reta. Entender que `clamp(MIN, VAL, MAX)` é `max(MIN, min(VAL, MAX))` com uma interpolação linear no meio muda tudo: você para de chutar números e passa a derivá-los. E há uma parte que quase ninguém conta, usar `vw` puro no `font-size` **quebra o critério WCAG 1.4.4 (Resize Text)**, porque o texto deixa de responder ao zoom. A correção não é abandonar o fluido; é somar uma componente em `rem`. Vamos derivar a fórmula, fazer as contas de verdade e montar o `clamp()` certo na [calculadora de clamp](tool:calculadora-clamp-css).

J-Kit13 min de leituraIntermediário
  • CSS
  • Tipografia fluida
  • Acessibilidade
  • WCAG
  • Design responsivo

Resumo rápido

  • `clamp(MIN, VAL, MAX)` equivale a `max(MIN, min(VAL, MAX))`: o valor preferido só vale entre os dois limites.
  • O valor preferido é uma reta: inclinação `m = (y2−y1)/(x2−x1)`, intersecção `b = y1 − m·x1`, resultado `b(rem) + 100m(vw)`.
  • `font-size` em `vw` puro quebra a WCAG 1.4.4, o texto não escala com o zoom. A componente em `rem` é o que devolve o controle ao usuário.
  • Sempre teste com zoom em 200% e com uma janela de 320px de largura antes de considerar um clamp pronto.

clamp() é interpolação linear disfarçada

A especificação CSS Values and Units Module Level 4 define `clamp(MIN, VAL, MAX)` como exatamente equivalente a `max(MIN, min(VAL, MAX))`. Leia da direita para a esquerda: o `min(VAL, MAX)` impede que o valor passe do teto; o `max(MIN, ...)` impede que caia abaixo do piso. No meio fica `VAL`, o "valor preferido", e é ali que mora o comportamento fluido. Se `VAL` fosse constante, `clamp()` não teria graça nenhuma; a curva só ganha vida porque `VAL` costuma depender da largura da tela, via unidades de viewport.

MIN (piso)
O menor valor permitido. Em telas estreitas, é ele que vence.
VAL (preferido)
A expressão fluida, a reta que interpola entre os limites. É onde entra o `vw`.
MAX (teto)
O maior valor permitido. Em telas largas, é ele que vence.

Repare que `clamp()` não faz nenhuma suavização, curva ou easing: entre o piso e o teto o crescimento é estritamente linear. Se você quer uma progressão com aceleração, texto que cresce devagar e depois dispara, `clamp()` sozinho não entrega; aí o terreno é o de curvas de tempo, que você molda no editor de cubic-bezier. Para tipografia responsiva, porém, a reta é justamente o que se quer: previsível e fácil de derivar.

Derivando a fórmula do zero

O problema é: quero um tamanho mínimo `y1` numa largura `x1` e um tamanho máximo `y2` numa largura maior `x2`, com uma reta ligando os dois pontos. Isso é geometria de ensino médio. A inclinação da reta é a variação vertical dividida pela horizontal; a intersecção é onde ela cruza o eixo em `x = 0`. A única sutileza de CSS é traduzir a reta para unidades que o navegador entende: a parte proporcional à largura vira `vw` (1vw = 1% da largura da janela), e a parte fixa vira `rem`.

m = (y₂ − y₁) / (x₂ − x₁) [inclinação / slope] b = y₁ − m · x₁ [intersecção em px / intercept in px] font-size = clamp( y₁ , b_rem + (100·m)vw , y₂ )
y₁
tamanho de fonte mínimo (o piso do clamp)
y₂
tamanho de fonte máximo (o teto do clamp)
x₁
largura de viewport onde a fonte deve ficar em y₁
x₂
largura de viewport onde a fonte deve atingir y₂
m
inclinação da reta, em px de fonte por px de largura
b
intersecção da reta em x = 0, em px
b_rem
a intersecção b convertida de px para rem (b ÷ tamanho raiz)
Como `1vw` é 1% da largura, um coeficiente de largura `m` (em px por px) vira `100·m` em vw. E `b`, calculado em px, é dividido pelo tamanho de fonte raiz para virar `b_rem` (por exemplo, dividido por 16 se a raiz é 16px).
  1. Escolha os quatro pontosDefina `y1` e `x1` (o par mínimo) e `y2` e `x2` (o par máximo). Trabalhe tudo em px para a conta.
  2. Calcule a inclinação`m = (y2 − y1) / (x2 − x1)`. Esse número é minúsculo, dezenas de milésimos, porque é px de fonte por px de largura.
  3. Calcule a intersecção`b = y1 − m · x1`. É o valor da reta em `x = 0`; pode ser positivo ou negativo.
  4. Traduza para CSSPreferido = `b_rem + (100·m)vw`. Monte `clamp(y1, preferido, y2)` com y1 e y2 em rem.

Dois exemplos com os números

Exemplo 1, corpo de texto. Quero 1rem (16px, com raiz de 16px) numa janela de 320px, subindo até 2rem (32px) numa janela de 1280px. Então `y1 = 16`, `x1 = 320`, `y2 = 32`, `x2 = 1280`. A inclinação é `m = (32 − 16) / (1280 − 320) = 16 / 960 = 0,016667`, que em vw é `100 × 0,016667 = 1,6667vw`. A intersecção é `b = 16 − 0,016667 × 320 = 16 − 5,3333 = 10,6667px`, que em rem é `10,6667 ÷ 16 = 0,6667rem`. Resultado: `clamp(1rem, 0.6667rem + 1.6667vw, 2rem)`.

1,6667vwcoeficiente de largura (100·m)
0,6667remintersecção b convertida para rem
clamp(1rem, 0.6667rem + 1.6667vw, 2rem)resultado final do Exemplo 1

Confira: numa janela de 320px, `1,6667vw = 5,3333px` e somando `0,6667rem = 10,6667px` dá exatamente 16px = 1rem. Numa janela de 1280px, `1,6667vw = 21,333px` mais 10,6667px dá 32px = 2rem. A reta passa pelos dois pontos que pedimos, e o `clamp()` congela nos limites fora do intervalo.

Exemplo 2, um título. Quero 1,5rem (24px) numa janela de 360px e 3rem (48px) numa janela de 1440px. Então `m = (48 − 24) / (1440 − 360) = 24 / 1080 = 0,022222`, ou seja `2,2222vw`. A intersecção é `b = 24 − 0,022222 × 360 = 24 − 8 = 16px`, que em rem é exatamente `16 ÷ 16 = 1rem`, um caso em que a conta sai redonda. Resultado: `clamp(1.5rem, 1rem + 2.2222vw, 3rem)`. Em 360px: `2,2222vw = 8px` mais 1rem (16px) = 24px = 1,5rem; em 1440px: `2,2222vw = 32px` mais 16px = 48px = 3rem.

:root {
  /* Example 1, body text: 16px (1rem) @ 320px  ->  32px (2rem) @ 1280px
     m = (32-16)/(1280-320) = 1/60 px/px   ->  100m = 1.6667vw
     b = 16 - (1/60)*320   = 10.6667px     ->  10.6667/16 = 0.6667rem */
  --fs-body: clamp(1rem, 0.6667rem + 1.6667vw, 2rem);

  /* Example 2, heading: 24px (1.5rem) @ 360px  ->  48px (3rem) @ 1440px
     m = (48-24)/(1440-360) = 1/45 px/px   ->  100m = 2.2222vw
     b = 24 - (1/45)*360   = 16px          ->  16/16 = 1rem */
  --fs-h1: clamp(1.5rem, 1rem + 2.2222vw, 3rem);
}

body { font-size: var(--fs-body); }
h1   { font-size: var(--fs-h1);   }
Os dois exemplos como custom properties. O termo em rem é obrigatório, o porquê vem na seção da WCAG.

A curva: platô, rampa, platô

Desenhar o tamanho da fonte em função da largura torna óbvio o que `clamp()` faz: um platô no mínimo enquanto a tela é estreita, uma rampa linear no intervalo escolhido e um platô no máximo quando a tela fica larga demais. O gráfico abaixo compara dois clamps. O conservador é o Exemplo 1 (16px→32px, entre 320px e 1280px). O agressivo é `clamp(1.5rem, 0.1207rem + 6.8966vw, 4rem)` (24px→64px, entre 320px e 900px): rampa muito mais íngreme, que satura cedo. Repare que ambos são retas entre dois joelhos, não há suavização.

0px16px32px48px64px2809401.600Largura do viewport (px)Tamanho da fonte (px)Conservador (16→32px)Agressivo (24→64px)
Tamanho da fonte (px) em função da largura do viewport (px), para um clamp conservador e um agressivo.
Ver os dados
xConservador (16→32px)Agressivo (24→64px)
28016px24px
32016px24px
48018,7px35px
64021,3px46,1px
80024px57,1px
96026,7px64px
1.28032px64px
1.60032px64px

Um clamp agressivo demais tem dois problemas: a fonte muda rápido demais entre dois tamanhos de tela próximos (desconfortável ao redimensionar) e ele satura cedo, desperdiçando a faixa de larguras maiores. Um clamp conservador respira melhor. Em vez de decorar os números, gere o seu e veja a curva na ferramenta abaixo.

Informe os pares mínimo e máximo; a calculadora devolve o clamp() já com o termo em rem.Abrir a ferramenta em página inteira

A armadilha da WCAG 1.4.4

Aqui está a parte que os tutoriais de "fonte que cresce" costumam pular. O critério WCAG 1.4.4 (Resize Text), nível AA, exige que o texto possa ser redimensionado até 200% sem perda de conteúdo ou funcionalidade, sem tecnologia assistiva, só com os controles do próprio navegador. E `font-size` em `vw` puro falha exatamente esse teste. Unidades de viewport são ancoradas ao tamanho da janela, não à preferência de fonte do usuário; quando alguém dá zoom de texto ou aumenta a fonte-base do navegador, o `vw` não se move. O usuário aperta "aumentar" e nada acontece.

Há uma segunda salvaguarda importante: não fixe um teto (`MAX`) baixo demais. Se o máximo do seu `clamp()` for menor que o dobro do tamanho-base, você impede o usuário de chegar aos 200% que a norma pede, mesmo com o termo em rem. Uma regra prática segura: só use um máximo se ele for pelo menos o dobro do mínimo, ou aceite não ter máximo nenhum. E teste de verdade, o critério exige verificação manual, redimensionando o texto e confirmando que ele cresce.

  1. 2016Nascem os "CSS Locks"

    Mike Riethmuller populariza a fórmula de interpolação linear com calc() e vw; Tim Brown a apelida de CSS Locks. É a mesma reta que o clamp() embute hoje.

  2. 2019Roselli documenta a falha de zoom

    Adrian Roselli publica "Responsive Type and Zoom", mostrando com testes que texto ancorado em vw impede o usuário de ampliar a fonte.

  3. 2020clamp() chega aos navegadores

    clamp(), min() e max() passam a ter suporte cruzado (Chrome 79, Firefox 75, Safari 13.1), tornando o CSS lock uma função nativa de uma linha.

  4. 2022Unidades svw / lvw / dvw

    As unidades de viewport pequena, grande e dinâmica chegam aos navegadores (Safari 15.4, Firefox 101, Chrome 108) para lidar com a barra de endereço móvel.

Sobre essas unidades novas: `vw` é igual a `lvw` (viewport grande, com a barra de endereço recolhida). `svw` mede a viewport pequena (barra visível) e `dvw` acompanha a mudança em tempo real. Elas existem porque, no celular, a barra de endereço aparece e some ao rolar, mudando a largura útil. Para `font-size`, isso raramente importa, a diferença é mínima na horizontal, e `dvw` pode até causar reflow ao rolar; o cuidado com essas unidades pesa mais em alturas (`dvh`) e em elementos que ocupam a tela inteira.

Além da tipografia (e a revisão)

O mesmo `clamp()` serve para qualquer propriedade que aceite comprimento: espaçamentos internos, gaps de grid e larguras de coluna que respiram com a tela sem uma pilha de media queries. Mas há detalhes que só aparecem quando você sai do `font-size`. O acordeão reúne os três que mais geram bug.

clamp() em padding, gap e largura de coluna

Para espaçamento, o padrão é `padding: clamp(1rem, 5vw, 3rem)`, margens que crescem com a tela mas nunca colam nas bordas nem explodem. Para larguras de conteúdo, `width: clamp(45ch, 60vw, 75ch)` mantém a linha de texto numa faixa legível (o `ch` é a largura do caractere "0"). Aqui a componente em rem é opcional: espaçamento e largura não estão sujeitos ao critério 1.4.4, que fala de texto. Mesmo assim, misturar uma unidade relativa à fonte (rem/em/ch) costuma dar resultados mais previsíveis.

Por que min() e max() sozinhos raramente bastam

`min(a, b)` retorna o menor dos valores e `max(a, b)` o maior. Um `width: min(90vw, 60rem)` limita a largura a 60rem em telas grandes, sem piso; um `max()` garante um mínimo, sem teto. O que quase todo layout responsivo quer, porém, é as duas coisas ao mesmo tempo: um piso e um teto com uma preferência fluida no meio. Fazer isso com min() e max() aninhados é escrever `max(MIN, min(VAL, MAX))`, que é literalmente a definição de clamp(). Ou seja: você pode, mas clamp() é o atalho legível para o caso comum.

A armadilha de inverter MIN e MAX

Se você trocar os limites, `clamp(MAX, VAL, MIN)` com o maior primeiro, o CSS não corrige por você. A especificação resolve `max(MIN, min(VAL, MAX))` ao pé da letra: quando `MIN > MAX`, o `min()` interno devolve MAX e o `max()` externo devolve MIN, então o resultado trava em MIN e ignora tudo. O texto fica preso num tamanho e você jura que o clamp "não funciona". Regra: o primeiro argumento é sempre o menor, o terceiro sempre o maior.

Antes de mandar para produção, passe pela revisão abaixo. Ela cobre os erros que não aparecem no seu monitor de 1440px mas explodem no celular de alguém ou no zoom de quem precisa dele.

  • O valor preferido inclui um termo em rem (a intersecção b_rem), nunca vw puro no font-size.
  • Testei com zoom em 200% e o texto de fato cresceu (WCAG 1.4.4).
  • Testei numa janela de 320px de largura e a fonte não ficou minúscula nem estourou o layout.
  • O máximo (MAX) é pelo menos o dobro do mínimo, ou não há máximo, para não travar o zoom antes dos 200%.
  • MIN é o primeiro argumento e MAX o último; conferi que não inverti.
  • Não usei clamp() cru em line-height sem cuidado: line-height sem unidade escala com a fonte; um clamp em px/rem congela o espaçamento entre linhas.

Perguntas frequentes

Como o clamp() funciona no CSS?
`clamp(MIN, VAL, MAX)` é definido pela especificação como `max(MIN, min(VAL, MAX))`: o resultado é o valor preferido VAL, mas nunca menor que MIN nem maior que MAX. Em tipografia fluida, VAL é uma reta em função da largura da tela (algo como `0.66rem + 1.67vw`), então a fonte cresce linearmente entre dois tamanhos de viewport e congela nos limites.
Por que não devo usar só vw no font-size?
Porque quebra a WCAG 1.4.4 (Resize Text): unidades de viewport estão presas ao tamanho da janela, não à preferência de fonte do usuário. Quando alguém dá zoom de texto ou aumenta a fonte-base, um `font-size: 4vw` não muda, a W3C documenta isso como a falha F94. A correção é somar um termo em rem no valor preferido, porque rem responde ao zoom e à configuração do navegador.
Como calcular os valores de um clamp() para fonte?
Escolha um tamanho mínimo y1 numa largura x1 e um máximo y2 numa largura x2. Calcule a inclinação `m = (y2−y1)/(x2−x1)` e a intersecção `b = y1 − m·x1` (tudo em px). O valor preferido é `b_rem + (100·m)vw`, onde b_rem é b dividido pelo tamanho de fonte raiz. O resultado é `clamp(y1, b_rem + 100m·vw, y2)`. A calculadora de clamp faz essa conta automaticamente.
Qual a diferença entre vw, svw, lvw e dvw?
`vw` é 1% da largura do viewport e equivale a `lvw` (viewport grande, com a barra de endereço recolhida). `svw` usa a viewport pequena (barra visível) e `dvw` acompanha a mudança em tempo real. Elas existem por causa da barra de endereço móvel, que aparece e some ao rolar. Para font-size a diferença horizontal é pequena; o cuidado é maior em alturas (dvh) e em telas cheias.
Posso usar clamp() em padding e largura, não só em fonte?
Sim. Qualquer propriedade que aceite comprimento funciona: `padding: clamp(1rem, 5vw, 3rem)` para espaçamento fluido, `width: clamp(45ch, 60vw, 75ch)` para uma coluna de texto legível, gaps de grid, e assim por diante. A exigência do termo em rem é específica de `font-size` (por causa da WCAG 1.4.4); em espaçamento e largura ela é opcional, embora misturar rem/em/ch ainda ajude na previsibilidade.

`clamp()` é uma reta com dois freios: derive a inclinação e a intersecção a partir dos seus pares mínimo e máximo, e nunca escreva o valor preferido só em vw. O termo em rem não é enfeite, é o que mantém o texto obediente ao zoom e ao critério WCAG 1.4.4. Faça a conta uma vez, teste em 200% e a 320px, e você tem tipografia fluida que também é acessível.

Fontes e referências

  1. MDN, clamp() (CSS Values and Units)
  2. W3C, Understanding SC 1.4.4: Resize Text
  3. W3C, F94: falha de 1.4.4 por uso incorreto de unidades de viewport
  4. Adrian Roselli, Responsive Type and Zoom (2019)
  5. MDN, unidades de comprimento (vw, svw, lvw, dvw)
  6. web.dev, The large, small, and dynamic viewport units