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

Sumário
- Os quatro parâmetros que explicam quase todo bug
- 1. Revelar quando entra na viewport
- 2. Amarrar a animação à barra de rolagem
- 3. Prender a seção enquanto algo acontece
- 4. Rolagem horizontal dentro da vertical
- 5. Revelar listas em lote
- 6. Encaixe entre seções
- 7. Indicador de progresso
- 8. Responsivo e com respeito a reduced-motion
- Os oito lado a lado, e onde o CSS já resolve
- O que quebra dentro do React
- Os erros que mais custam tempo
- Quando as animações se atropelam
- Perguntas frequentes
- Fontes
A documentação do ScrollTrigger tem dezenas de propriedades. Na prática, oito combinações resolvem quase tudo que aparece em site de agência, e as outras existem para casos que você vai encontrar uma vez por ano.
Este post traz as oito com o código mínimo de cada uma, os valores padrão que explicam por que sua animação dispara na hora errada, e a parte que a maioria dos tutoriais pula: o que acontece quando você põe isso dentro de um componente React.
Nota de método: os "90%" do título são julgamento editorial baseado nos projetos de motion que já entreguei, não uma medição. Não existe levantamento público de frequência de uso de propriedades do ScrollTrigger, e eu não fiz um. Trate o número como recorte de prioridade, não como estatística.
Tudo aqui foi conferido contra a documentação oficial na versão 3.15.0, publicada em 13 de abril de 2026.
Os quatro parâmetros que explicam quase todo bug
Antes das receitas, quatro valores padrão. Conhecer os quatro elimina a maior parte das sessões de tentativa e erro com markers: true ligado.
| Propriedade | Padrão | O que significa |
|---|---|---|
start | "top bottom", ou "top top" se houver pin | O topo do trigger encosta na base da viewport |
end | "bottom top" | A base do trigger passa pelo topo da viewport |
toggleActions | "play none none none" | Ações nas quatro bordas: entrar, sair, voltar entrando, voltar saindo |
pinSpacing | true, e false quando o container é display: flex | Espaço reservado para o conteúdo não pular quando o pin solta |
O start padrão é o que mais confunde. "top bottom" significa que a animação dispara no instante em que o topo do elemento toca a base da tela, ou seja, assim que ele começa a aparecer. Quase sempre o que você quer é deixar o elemento entrar de fato antes de animar:
gsap.from(".card", {
y: 40,
opacity: 0,
scrollTrigger: {
trigger: ".card",
start: "top 85%", // o topo do card na altura de 85% da viewport
},
});Enquanto estiver ajustando, markers: true desenha as linhas de start e end na tela. Tire antes de subir.
1. Revelar quando entra na viewport
O padrão mais comum, e o que precisa de menos configuração. toggleActions controla o que acontece nas quatro bordas, na ordem: entrar, sair, voltar entrando, voltar saindo.
gsap.from(".secao", {
y: 60,
opacity: 0,
duration: 0.8,
ease: "power2.out",
scrollTrigger: {
trigger: ".secao",
start: "top 80%",
toggleActions: "play none none reverse",
},
});Com "play none none reverse", o elemento anima ao entrar e desfaz quando você rola de volta para cima. Se a reveleção deve acontecer uma vez e ficar, existe atalho:
scrollTrigger: { trigger: ".secao", start: "top 80%", once: true }O once mata o trigger depois que ele chega ao fim, o que também tira o custo de monitoramento. Para uma lista com dezenas de elementos, isso importa.
2. Amarrar a animação à barra de rolagem
Aqui a animação deixa de ter duração própria e passa a ser controlada pela posição do scroll.
gsap.to(".painel", {
xPercent: -100,
ease: "none",
scrollTrigger: {
trigger: ".painel",
start: "top top",
end: "+=1500",
scrub: 1,
},
});A escolha entre scrub: true e scrub: 1 muda a sensação inteira. Com true, a animação segue a barra sem atraso nenhum, e todo tranco do dedo no trackpad aparece na tela. Com um número, você define quantos segundos o playhead leva para alcançar a posição da barra, o que suaviza a leitura sem mexer no scroll da página.
scrub: 0.5 a scrub: 1.5 cobre quase todo caso real. Acima disso, a animação começa a parecer desconectada do gesto.
Com scrub, use ease: "none" na animação. Easing e scrub competem pelo mesmo controle e o resultado fica errático.
3. Prender a seção enquanto algo acontece
O pin é o que faz uma seção travar na tela enquanto o conteúdo dela avança.
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "+=2000",
pin: true,
scrub: 1,
},
});
tl.to(".hero-titulo", { scale: 1.4, opacity: 0 })
.from(".hero-detalhe", { yPercent: 40, opacity: 0 }, "<");Duas coisas que a documentação avisa e que se paga conhecer antes de depurar.
Não anime o elemento pinado. O ScrollTrigger pré-calcula as posições para render mais rápido, e mexer no elemento que ele está medindo joga a conta fora. Anime os filhos, como no exemplo acima.
pinSpacing reserva o espaço. Por padrão ele adiciona padding na base, ou à direita em horizontal: true, para que o conteúdo seguinte não salte quando o pin solta. A exceção é quando o container usa display: flex, caso em que ele vem como false e você precisa resolver o espaçamento na mão.
4. Rolagem horizontal dentro da vertical
O efeito de galeria que anda para o lado enquanto você rola para baixo. A parte que quase ninguém implementa direito é disparar animações dentro dessa faixa horizontal.
const faixa = gsap.to(".trilho", {
x: () => -(document.querySelector(".trilho").scrollWidth - innerWidth),
ease: "none",
scrollTrigger: {
trigger: ".galeria",
start: "top top",
end: () => "+=" + document.querySelector(".trilho").scrollWidth,
pin: true,
scrub: 1,
invalidateOnRefresh: true,
},
});
gsap.from(".item-galeria", {
opacity: 0,
scale: 0.9,
stagger: 0.2,
scrollTrigger: {
trigger: ".item-galeria",
containerAnimation: faixa,
start: "left 80%",
},
});O containerAnimation diz ao ScrollTrigger para observar a animação do trilho em vez da barra de rolagem, já que movimento horizontal por transform não é scroll de verdade.
Três limitações documentadas, e todas mordem:
- A animação do container precisa de ease linear (
ease: "none"). - Pin e snap não funcionam em triggers baseados em
containerAnimation. - Evite animar na horizontal o próprio elemento de trigger, ou compense nos valores de start e end.
O invalidateOnRefresh: true no primeiro trigger recalcula as larguras quando a janela muda de tamanho. Sem ele, girar o celular quebra o cálculo.
5. Revelar listas em lote
Aplicar um trigger por card de uma grade de trinta itens funciona e fica feio: cada card anima sozinho, sem relação com os vizinhos que entraram no mesmo instante.
ScrollTrigger.batch() resolve agrupando os elementos que dispararam o mesmo callback dentro de uma janela de tempo.
ScrollTrigger.batch(".card", {
start: "top 88%",
interval: 0.1,
batchMax: 4,
onEnter: (elementos) =>
gsap.to(elementos, { opacity: 1, y: 0, stagger: 0.15, overwrite: true }),
});O callback recebe dois argumentos: o array de elementos que entraram naquela janela e os triggers correspondentes. interval define o tamanho da janela de coleta em segundos e batchMax limita quantos elementos entram por lote, aceitando também uma função para layouts responsivos.
O batch() aceita as propriedades normais de ScrollTrigger, com exceção das ligadas a animação, como animation, scrub e snap, e do próprio trigger. Ele monta o trigger de cada elemento por conta.
6. Encaixe entre seções
O snap faz o scroll acomodar em posições específicas quando o usuário para de rolar. Usado com moderação, dá acabamento. Usado demais, briga com o usuário.
ScrollTrigger.create({
trigger: ".secoes",
start: "top top",
end: "bottom bottom",
snap: {
snapTo: 1 / 4, // cinco pontos: 0, 0.25, 0.5, 0.75, 1
duration: { min: 0.2, max: 0.6 },
delay: 0.1,
ease: "power1.inOut",
directional: true,
},
});snapTo aceita número (incrementos), array de valores, função com lógica própria, ou as palavras "labels" e "labelsDirectional" quando você quer encaixar nos rótulos de uma timeline. O padrão de directional é true, o que significa que o encaixe respeita o sentido em que você estava rolando, e o padrão de ease é "power3".
O duration com {min, max} é o detalhe que separa snap tolerável de snap irritante: ele impede que um encaixe curto demore o mesmo que um longo.
7. Indicador de progresso
Barra de leitura no topo, contador de seção, qualquer coisa que precise saber o quanto já rolou. O onUpdate dispara a cada mudança de progresso e recebe a própria instância.
ScrollTrigger.create({
trigger: "article",
start: "top top",
end: "bottom bottom",
onUpdate: (self) => {
gsap.set(".barra-progresso", { scaleX: self.progress });
},
});self.progress vai de 0 a 1. Animar scaleX com transform-origin: left mantém o trabalho na GPU, ao contrário de animar width, que força o navegador a recalcular layout a cada quadro. O post sobre CSS scroll-driven sem JS mostra que esse caso específico já tem solução nativa em CSS, sem nenhuma biblioteca.
Vale saber que existe também onToggle, que dispara só quando o estado ativo muda, e onRefresh, quando as posições são recalculadas. Para um indicador, onUpdate é o certo. Para acender e apagar uma classe de menu, onToggle gasta muito menos.
8. Responsivo e com respeito a reduced-motion
O último padrão é o que menos aparece em tutorial e o que mais evita retrabalho.
ScrollTrigger.matchMedia() está descontinuado. O substituto é gsap.matchMedia(), que envolve e supera a versão antiga, e reverte sozinho animações e ScrollTriggers criados dentro dele.
const mm = gsap.matchMedia();
mm.add(
{
isDesktop: "(min-width: 900px)",
isMobile: "(max-width: 899px)",
reduzMovimento: "(prefers-reduced-motion: reduce)",
},
(context) => {
const { isDesktop, reduzMovimento } = context.conditions;
if (reduzMovimento) {
gsap.set(".secao", { opacity: 1, y: 0 });
return;
}
gsap.from(".secao", {
y: isDesktop ? 60 : 24,
opacity: 0,
scrollTrigger: { trigger: ".secao", start: "top 85%" },
});
}
);O prefers-reduced-motion: reduce sinaliza que a pessoa ligou, no sistema operacional, a preferência por minimizar movimento não essencial. A finalidade documentada é atender quem tem distúrbios vestibulares, para quem escalar ou deslocar objetos grandes provoca desconforto real. O valor no-preference avalia como falso e significa apenas que ninguém declarou preferência.
Atender isso custa as seis linhas do exemplo. Ignorar custa uma pessoa passando mal no seu site. O tratamento completo, com a tabela de substituição por tipo de efeito, está em prefers-reduced-motion sem matar o design.
Os oito lado a lado, e onde o CSS já resolve
Metade destes padrões tem equivalente nativo hoje. Vale conferir antes de instalar biblioteca, porque animação em CSS scroll-driven roda na thread do compositor e não depende de JavaScript carregar.
| # | Padrão | Propriedade central | Equivalente em CSS |
|---|---|---|---|
| 1 | Revelar ao entrar | toggleActions, once | animation-timeline: view() |
| 2 | Amarrar ao scroll | scrub | animation-timeline: scroll() |
| 3 | Prender a seção | pin, pinSpacing | Nenhum. position: sticky prende, mas não dá progresso |
| 4 | Horizontal na vertical | containerAnimation | Nenhum |
| 5 | Revelar em lote | ScrollTrigger.batch() | Parcial, com animation-delay na mão |
| 6 | Encaixe entre seções | snap | scroll-snap-type, suporte antigo e amplo |
| 7 | Barra de progresso | onUpdate | animation-timeline: scroll() |
| 8 | Responsivo e acessível | gsap.matchMedia() | @media, direto |
Os padrões 3, 4 e 5 são a razão real de manter o GSAP num projeto. Os outros cinco cabem em CSS quando o alvo permite, com a ressalva de que o Firefox só liga o suporte por preferência, tratada em detalhe no post sobre CSS scroll-driven.
Um exemplo de projeto com pin, faixa horizontal e reveleção em lote convivendo na mesma página está no caso da Plantica, onde o custo de cada decisão aparece com o resultado.
O que quebra dentro do React
Essa seção existe porque a maior parte dos exemplos de ScrollTrigger é escrita para HTML solto, e o público que constrói site premium hoje está em Next.js.
O problema concreto: o Strict Mode do React roda os efeitos duas vezes em desenvolvimento. Sem limpeza, você termina com dois ScrollTriggers no mesmo elemento, brigando pela mesma medição.
O pacote @gsap/react traz o hook useGSAP(), que reverte sozinho toda animação, ScrollTrigger, Draggable e SplitText criados durante a execução quando o componente desmonta.
import { useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(ScrollTrigger, useGSAP);
export function Secao() {
const container = useRef(null);
useGSAP(
() => {
gsap.from(".card", {
y: 40,
opacity: 0,
stagger: 0.1,
scrollTrigger: { trigger: ".card", start: "top 85%" },
});
},
{ scope: container }
);
return <section ref={container}>{/* ... */}</section>;
}Duas opções do hook mudam o comportamento. scope recebe um ref e limita todo seletor de texto aos descendentes daquele container, o que evita que ".card" pegue cards de outro componente. dependencies controla quando o bloco roda de novo, com array vazio por padrão, e revertOnUpdate decide se o contexto reverte quando as dependências mudam.
Uma armadilha que sobra: animação criada depois da execução do hook, dentro de um onClick ou de um setTimeout, não entra na limpeza. Para essas, envolva a função com contextSafe().
Os erros que mais custam tempo
Cinco coisas que eu já depurei mais de uma vez e que têm causa documentada.
A animação dispara antes da hora. É o start padrão "top bottom". Troque por uma porcentagem, como "top 85%".
O conteúdo pula quando o pin solta. Container em display: flex derruba o pinSpacing para false. Ou reserve o espaço na mão, ou tire o flex do container que está sendo pinado.
A medição sai errada em página com imagem. As posições são calculadas antes de as imagens carregarem e mudarem a altura do documento. Defina width e height nas imagens, ou chame ScrollTrigger.refresh() depois do carregamento.
Tudo desalinha depois de girar o celular. Falta invalidateOnRefresh: true nos triggers cujos valores de start e end dependem de medida da tela.
A animação está travada e você não sabe por quê. Ligue markers: true, veja onde as linhas caem, e confira se o elemento de trigger é o que você imagina. Na metade das vezes, é.
Para decidir se o ScrollTrigger é a ferramenta certa antes de escrever qualquer uma dessas linhas, a árvore de decisão entre GSAP, Motion e CSS puro resolve em três perguntas. E o guia completo de motion design para web cobre o contexto maior, do briefing à entrega.
Quando as animações se atropelam
Existe um sintoma que aparece só em teste real e nunca no desenvolvimento: a pessoa rola rápido, passa por cinco seções em um gesto, e as cinco animações começam ao mesmo tempo e terminam empilhadas. O resultado parece bug e é comportamento esperado.
Três propriedades tratam disso, e nenhuma delas costuma aparecer em tutorial.
fastScrollEnd força a animação do trigger atual a completar quando a pessoa sai da área de gatilho acima de uma certa velocidade. O padrão é 2500 pixels por segundo, e você pode exigir mais, como fastScrollEnd: 3000, para que só uma rolagem bem rápida acione o corte.
preventOverlaps age no instante em que um trigger vai começar: ele procura animações anteriores baseadas em scroll e as força ao estado final. Aceita true, que afeta todas as anteriores, ou uma string como identificador, quando você quer que só as animações do mesmo grupo se cancelem.
gsap.from(".secao", {
y: 60,
opacity: 0,
scrollTrigger: {
trigger: ".secao",
start: "top 85%",
fastScrollEnd: true,
preventOverlaps: "reveal",
},
});refreshPriority resolve outro atropelamento, o do cálculo. Um trigger com refreshPriority: 1 é recalculado antes de um com 0, que é o padrão. A documentação recomenda antes criar os ScrollTriggers na ordem da página, de cima para baixo, e reservar a propriedade para quando isso não for possível.
A ordem de criação importa porque um pin empurra tudo que vem depois. Se o trigger de baixo foi criado primeiro, ele mede um documento que ainda não sabe do espaço que o pin vai reservar.
Perguntas frequentes
O ScrollTrigger é pago?
Não. Ele nunca foi plugin premium, e desde a mudança de licenciamento o pacote inteiro do GSAP sai sob a licença "no charge". O detalhe de quem detém a propriedade e quais cláusulas continuam valendo está em GSAP 100% grátis: o que mudou.
Qual a diferença entre scrub: true e scrub: 1?
Com true, a animação acompanha a barra de rolagem sem atraso, e cada tranco do gesto aparece. Com um número, esse número é quanto tempo em segundos o playhead leva para alcançar a posição da barra, o que suaviza a leitura. Entre 0.5 e 1.5 cobre a maioria dos casos.
Por que meu pin faz o conteúdo seguinte saltar?
Porque o pinSpacing não está reservando espaço. Ele vem como true por padrão e adiciona padding na base, mas cai para false quando o container usa display: flex. Nesse caso, o espaço precisa ser resolvido no CSS.
Dá para usar snap junto com rolagem horizontal?
Não em triggers baseados em containerAnimation. A documentação declara que pin e snap não estão disponíveis nesse modo, porque o movimento horizontal vem de um transform e não da barra de rolagem. O encaixe precisa ser construído na animação do container.
Fontes
- GSAP — ScrollTrigger: documentação da API. Acesso em 20/08/2026.
- GSAP — ScrollTrigger.batch()/). Acesso em 20/08/2026.
- GSAP — gsap.matchMedia()/). Acesso em 20/08/2026.
- GSAP — GSAP com React e o hook useGSAP(). Acesso em 20/08/2026.
- MDN Web Docs — prefers-reduced-motion. Acesso em 20/08/2026.
- npm — registro do pacote gsap, versão 3.15.0 publicada em 13/04/2026. Acesso em 20/08/2026.
Verificado em 20 de agosto de 2026.
Gatilho de reavaliação: revisar quando (a) o GSAP publicar uma major que mude a API do ScrollTrigger, (b) containerAnimation passar a aceitar pin ou snap, ou (c) o useGSAP() mudar de pacote ou de assinatura.
Leia também
Motion •
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
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 •
View Transitions no Next.js 16: as três camadas
Uma é Baseline, outra não roda no Firefox e a terceira é experimental no React. Saber qual você está usando decide o que vai para produção.
- next.js
- react


