Animação de texto: SplitText, clip-path e o que o CSS já faz
Metade dos efeitos de texto não precisa quebrar o texto em pedaços. A pergunta que separa as duas famílias, e o que o SplitText resolve que o CSS não resolve.
- motion design
- css

Sumário
- A pergunta que separa as duas famílias
- Sem quebrar o texto: o que o CSS já entrega
- Quando quebrar é inevitável
- Quando quebrar é a resposta errada
- Os dois problemas que quase ninguém menciona
- Acessibilidade: o padrão mudou, e para melhor
- propIndex: a ponte entre as duas famílias
- Custo e limpeza
- Perguntas frequentes
- Fontes
Uma pergunta separa quase todo efeito de texto em duas famílias: os pedaços precisam se mover de forma independente?
Se não, o CSS resolve sozinho, com uma linha e sem biblioteca. Se sim, você precisa quebrar o texto em elementos, e aí não existe equivalente nativo. O erro comum é instalar a biblioteca para fazer o que a primeira família já resolvia.
A pergunta que separa as duas famílias
Antes de escolher ferramenta, classifique o efeito.
| Efeito | Precisa quebrar? | Como fazer |
|---|---|---|
| Revelação de baixo para cima, bloco inteiro | Não | clip-path ou transform |
| Gradiente correndo dentro das letras | Não | background-clip: text |
| Texto surgindo por trás de uma máscara | Não | clip-path ou mask-image |
| Sublinhado que cresce no hover | Não | transform: scaleX |
| Cada linha entrando com atraso | Sim | SplitText, tipo lines |
| Cada palavra com atraso | Sim | SplitText, tipo words |
| Cada caractere com atraso ou posição própria | Sim | SplitText, tipo chars |
| Letra individual reagindo ao mouse | Sim | SplitText, tipo chars |
As quatro primeiras linhas são a maioria do que se pede num site institucional. Elas não precisam de biblioteca nenhuma.
Sem quebrar o texto: o que o CSS já entrega
Revelação com clip-path. A propriedade é Baseline desde janeiro de 2020, e as formas básicas são animáveis com interpolação suave. Um inset() que sai de 100% para 0% dá a revelação clássica:
.titulo {
clip-path: inset(0 0 100% 0);
transition: clip-path .8s cubic-bezier(.2,.8,.2,1);
}
.titulo.visivel {
clip-path: inset(0 0 0 0);
}Duas ressalvas documentadas. Valores em url(), apontando para um <clipPath> de SVG, não são animáveis: só as formas básicas interpolam. E qualquer valor diferente de none cria um novo contexto de empilhamento, o mesmo efeito colateral de uma opacidade diferente de 1.
Gradiente dentro das letras com background-clip: text. Baseline desde julho de 2015. O texto precisa ficar transparente para o fundo aparecer:
.gradiente {
background: linear-gradient(90deg, #4f6420, #c4552f);
background-clip: text;
color: transparent;
text-shadow: none;
}A MDN é explícita sobre três cuidados aqui, e eles não são teóricos:
- Confira o contraste entre o fundo e o texto, porque quem tem baixa visão precisa ler.
- Declare uma
background-colorde reserva, para o texto não sumir se a imagem de fundo falhar. - Use
@supports (background-clip: text)e ofereça alternativa acessível onde não houver suporte.
Revelação com mask-image. É a terceira técnica nativa, e a mais flexível das três, porque o gradiente da máscara controla a suavidade da borda. Diferente do clip-path, que corta seco, a máscara permite uma transição esfumaçada:
.linha {
mask-image: linear-gradient(to right, #000 0 0, transparent 0 100%);
mask-size: 200% 100%;
mask-position: 100% 0;
transition: mask-position 1s ease;
}
.linha.visivel { mask-position: 0 0; }O que anima aqui é a posição da máscara, não a forma. Isso evita o contexto de empilhamento novo que o clip-path cria, e mantém o trabalho em propriedades que o compositor resolve.
Escolha entre as duas assim: clip-path quando a borda do corte é reta e nítida, mask-image quando você quer degradê ou uma forma irregular.
Revelação ligada ao scroll, sem JavaScript. Para o caso mais comum de todos, aparecer conforme entra na tela, existe animation-timeline: view(), que roda na thread do compositor. O detalhamento está em CSS scroll-driven sem JS.
Quando quebrar é inevitável
Não existe, hoje, um jeito em CSS de dividir um parágrafo em linhas, palavras ou caracteres endereçáveis. ::first-line alcança a primeira linha e para por aí. Para atraso escalonado por pedaço, o texto precisa virar elementos, e isso é JavaScript.
O SplitText do GSAP é a ferramenta madura para isso, e desde a mudança de licenciamento ele vem no pacote público. Confirmei lendo o arquivo publicado na versão 3.15.0: o cabeçalho traz a licença "no charge" da GreenSock, a mesma que o post sobre o GSAP gratuito detalha.
O uso mínimo:
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
SplitText.create(".titulo", {
type: "lines",
mask: "lines",
autoSplit: true,
onSplit(self) {
return gsap.from(self.lines, {
yPercent: 100,
opacity: 0,
stagger: 0.08,
duration: 0.9,
ease: "power3.out",
onComplete: () => self.revert(),
});
},
});Quatro escolhas nesse bloco merecem explicação.
type aceita chars, words e lines, combinados por vírgula, e o padrão é os três juntos. Pedir os três quando você só anima linhas cria três vezes mais elementos do que o necessário. Peça só o que vai animar.
mask embrulha cada pedaço num elemento extra que recorta o conteúdo. É o que faz a linha "subir de dentro" em vez de deslizar por cima do resto. Sem isso, você teria que montar o overflow: hidden na mão. Ele aceita um tipo só por vez.
onSplit é onde a animação deve nascer, e não é detalhe estético: ele roda de novo a cada nova divisão, o que importa por causa do próximo item.
revert() devolve o innerHTML original. Depois que a animação termina, os elementos extras não servem para nada e continuam pesando no DOM e na árvore de acessibilidade.
Os outros campos que valem conhecer
A configuração vai além dos quatro do exemplo. Estes são os que eu uso de fato:
| Campo | Padrão | Para que serve |
|---|---|---|
type | "chars,words,lines" | Quais pedaços criar. Peça só o que vai animar |
mask | ausente | Envolve cada pedaço num recorte. Um tipo por vez |
autoSplit | false | Divide de novo quando a fonte carrega ou a largura muda |
aria | "auto" | aria-label no pai, aria-hidden nos pedaços |
linesClass | ausente | Classe nas linhas. Com "++" no fim, numera sozinho |
wordsClass | ausente | Idem, para palavras |
charsClass | ausente | Idem, para caracteres |
propIndex | false | Cria variáveis CSS com o índice de cada pedaço |
smartWrap | false | Evita quebra dentro da palavra em divisão só por caractere |
ignore | ausente | Elementos descendentes que ficam de fora da divisão |
deepSlice | true | Subdivide elemento aninhado que atravessa duas linhas |
reduceWhiteSpace | true | Colapsa espaços consecutivos em um só |
tag | "div" | Elemento usado como invólucro dos pedaços |
wordDelimiter | espaço | Separador de palavra, aceita string ou expressão regular |
Dois deles resolvem irritações específicas. O sufixo "++" nas classes, como charsClass: "letra++", gera letra1, letra2 e assim por diante, o que permite mirar um pedaço específico no CSS sem contar filhos. E ignore mantém intacto um elemento dentro do bloco, útil quando o título tem um <span> de marca ou um ícone que não deve virar caractere solto.
Quando quebrar é a resposta errada
Precisar dos pedaços não basta para justificar a divisão. Três situações em que eu recuo mesmo tendo o efeito em mente.
Texto longo de leitura. Um artigo, uma descrição de produto, qualquer bloco que a pessoa vai ler de verdade. Dividir multiplica o DOM e, mais importante, mexe na árvore de acessibilidade de um conteúdo que precisa ser lido, não admirado. Reserve a divisão para título, chamada e número de destaque.
Texto que muda depois de montado. Em componente que re-renderiza, cada atualização precisa reverter e dividir de novo, ou os pedaços ficam dessincronizados do conteúdo. Dá para fazer, e o custo de manutenção raramente compensa um efeito decorativo. Quando o texto vem de estado, prefira animar o bloco.
Texto que precisa ser copiado. Divisão por caractere insere elementos entre as letras. Dependendo de como você monta os invólucros, a seleção com o mouse e o copiar-colar saem com espaçamento estranho. Vale testar selecionando o trecho antes de aprovar, porque é o tipo de defeito que ninguém percebe até um cliente reclamar.
O campo reduceWhiteSpace, ligado por padrão, ajuda no terceiro caso ao colapsar espaços consecutivos, e não resolve sozinho. Teste a seleção.
Os dois problemas que quase ninguém menciona
O primeiro é a fonte que carrega depois. Você divide o texto em linhas, a fonte da web termina de carregar, as métricas mudam, o texto reflui, e a divisão que você fez já não corresponde às linhas que aparecem na tela. O resultado é quebra em lugar errado, às vezes uma linha órfã com uma palavra.
autoSplit: true resolve: ele reverte e divide de novo quando as fontes terminam de carregar ou quando a largura do elemento muda. É por isso que a animação precisa nascer dentro de onSplit, para ser recriada junto. O padrão do campo é false, então é opt-in.
O segundo é como o texto é cortado em caracteres. Dividir uma string por posição quebra emoji, letras com marcas combinantes e escritas não latinas, porque um caractere visível nem sempre é uma unidade de código. Lendo o fonte publicado da 3.15.0, o SplitText instancia Intl.Segmenter quando o navegador oferece, e usa isso para segmentar. Na prática, um emoji composto continua inteiro em vez de virar dois pedaços sem sentido.
Para português isso importa pouco no dia a dia, e importa muito no momento em que alguém coloca um emoji no título do hero.
Acessibilidade: o padrão mudou, e para melhor
A crítica histórica a dividir texto é que a árvore de acessibilidade vira lixo: o leitor de tela passa a anunciar letra por letra, ou palavra solta sem pontuação.
O SplitText trata isso por padrão. O campo aria vem como "auto", o que coloca um aria-label com o texto original no elemento pai e marca os pedaços como aria-hidden. O leitor de tela lê a frase, não os cacos. Os outros valores são "hidden", que só esconde os pedaços, e "none", que não mexe em nada.
Vale saber que o padrão é esse, e vale mais ainda não trocar para "none" sem motivo.
Do lado do movimento, animação de texto é um dos casos onde prefers-reduced-motion pesa: caractere entrando em posição aleatória é exatamente o tipo de deslocamento que incomoda. O caminho é substituir, não desligar:
const mm = gsap.matchMedia();
mm.add({
reduzir: "(prefers-reduced-motion: reduce)",
normal: "(prefers-reduced-motion: no-preference)",
}, (ctx) => {
const { reduzir } = ctx.conditions;
SplitText.create(".titulo", {
type: reduzir ? "lines" : "chars,words,lines",
autoSplit: true,
onSplit(self) {
return gsap.from(reduzir ? self.lines : self.chars, {
opacity: 0,
y: reduzir ? 0 : 40,
stagger: reduzir ? 0.04 : 0.02,
duration: reduzir ? 0.3 : 0.8,
});
},
});
});Com a preferência ligada, o efeito vira um fade por linha, sem deslocamento. A tabela completa de substituição por tipo de efeito está em prefers-reduced-motion.
propIndex: a ponte entre as duas famílias
Um campo pouco usado do SplitText fecha a distância entre quebrar o texto e animar em CSS.
Com propIndex: true, cada pedaço recebe uma variável CSS com o índice dele, como --word: 1, --word: 2 e assim por diante. Você usa o JavaScript só para quebrar, e deixa o CSS cuidar do atraso:
.palavra {
animation: entra .6s both;
animation-delay: calc(var(--word) * 40ms);
}
@keyframes entra {
from { opacity: 0; transform: translateY(12px); }
}O ganho é de arquitetura. O escalonamento vira responsabilidade da folha de estilo, muda com media query sem tocar em JavaScript, e desaparece dentro de um bloco de prefers-reduced-motion sem condicional no código.
Custo e limpeza
Dividir texto multiplica o DOM. Um parágrafo de 40 palavras com type: "chars,words,lines" produz algumas centenas de elementos, cada um com estilo próprio. Três hábitos evitam o problema.
Peça só o tipo que vai animar. type: "lines" para revelação por linha, e nada além disso.
Reverta ao terminar. onComplete: () => self.revert() devolve o innerHTML original e limpa tudo, como no exemplo do começo.
Divida só o que está visível. Um ScrollTrigger que dispara a criação perto da viewport evita quebrar dez blocos de texto no carregamento. Os padrões estão em ScrollTrigger: os 8 padrões.
Para a decisão anterior a tudo isso, se o projeto deve carregar GSAP, a árvore de decisão entre GSAP, Motion e CSS puro resolve em três perguntas. E o contexto maior está no guia completo de motion design para web.
Perguntas frequentes
Dá para dividir texto em linhas só com CSS?
Não. O CSS alcança a primeira linha com ::first-line e não oferece um jeito de endereçar linha por linha, palavra por palavra ou caractere por caractere. Para atraso escalonado por pedaço, o texto precisa virar elementos, e isso continua sendo trabalho de JavaScript.
O SplitText é pago?
Não. Ele vem no pacote público do GSAP sob a licença "no charge" da GreenSock, confirmável no cabeçalho do arquivo publicado. A licença tem cláusulas próprias e não é open source, o que está detalhado em GSAP 100% grátis: o que mudou.
Por que minha divisão em linhas quebra errado?
Quase sempre é a fonte da web terminando de carregar depois da divisão, o que muda as métricas e reflui o texto. autoSplit: true reverte e divide de novo quando as fontes carregam ou a largura muda. Como ele recria os elementos, a animação precisa ser criada dentro do callback onSplit para ser recriada junto.
Texto dividido atrapalha leitor de tela?
Por padrão, não. O campo aria vem como "auto", que põe um aria-label com o texto original no elemento pai e marca os pedaços como aria-hidden, de modo que o leitor anuncia a frase inteira. O cuidado é não mudar esse valor para "none" sem ter um motivo e uma alternativa.
Fontes
- GSAP — SplitText: documentação da API. Acesso em 20/08/2026.
- GSAP — Arquivo publicado do SplitText 3.15.0, consultado para confirmar licença e uso de
Intl.Segmenter. Acesso em 20/08/2026. - MDN Web Docs — clip-path. Acesso em 20/08/2026.
- MDN Web Docs — background-clip. Acesso em 20/08/2026.
- MDN Web Docs — prefers-reduced-motion. Acesso em 20/08/2026.
Verificado em 20 de agosto de 2026.
Gatilho de reavaliação: revisar quando (a) o CSS ganhar um mecanismo nativo de segmentação de texto endereçável, (b) o padrão do campo aria do SplitText mudar, ou (c) background-clip: text deixar de exigir color: transparent.
Leia também
Motion •
prefers-reduced-motion: motion acessível sem matar o design
A palavra da especificação é reduzir, não remover. Por que animation: none !important erra dos dois lados, e a tabela de substituição que preserva o design.
- acessibilidade
- motion design
Motion •
ScrollTrigger: os 8 padrões que cobrem 90% dos casos
Oito receitas de ScrollTrigger com o código mínimo de cada uma, os padrões que quase ninguém lê na doc e o que quebra quando você põe tudo dentro do React.
- gsap
- scrolltrigger
Motion •
GSAP, Motion ou CSS: a árvore de decisão
A pergunta mudou: metade do que exigia biblioteca hoje é CSS nativo. Os eixos que sobraram são licença, modelo de execução e o que já é nativo.
- gsap
- motion


