Pular para o conteúdo
Zumkai

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
Card com os oito padrões de ScrollTrigger e os valores padrão de start, end e toggleActions.
Sumário
  1. Os quatro parâmetros que explicam quase todo bug
  2. 1. Revelar quando entra na viewport
  3. 2. Amarrar a animação à barra de rolagem
  4. 3. Prender a seção enquanto algo acontece
  5. 4. Rolagem horizontal dentro da vertical
  6. 5. Revelar listas em lote
  7. 6. Encaixe entre seções
  8. 7. Indicador de progresso
  9. 8. Responsivo e com respeito a reduced-motion
  10. Os oito lado a lado, e onde o CSS já resolve
  11. O que quebra dentro do React
  12. Os erros que mais custam tempo
  13. Quando as animações se atropelam
  14. Perguntas frequentes
  15. 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.

PropriedadePadrãoO que significa
start"top bottom", ou "top top" se houver pinO 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
pinSpacingtrue, e false quando o container é display: flexEspaç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:

js
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.

js
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:

js
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.

js
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.

js
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.

js
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.

js
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.

js
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.

js
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.

js
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ãoPropriedade centralEquivalente em CSS
1Revelar ao entrartoggleActions, onceanimation-timeline: view()
2Amarrar ao scrollscrubanimation-timeline: scroll()
3Prender a seçãopin, pinSpacingNenhum. position: sticky prende, mas não dá progresso
4Horizontal na verticalcontainerAnimationNenhum
5Revelar em loteScrollTrigger.batch()Parcial, com animation-delay na mão
6Encaixe entre seçõessnapscroll-snap-type, suporte antigo e amplo
7Barra de progressoonUpdateanimation-timeline: scroll()
8Responsivo e acessívelgsap.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.

jsx
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.

js
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

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