Pular para o conteúdo
  • Sites
  • GSAP
  • ScrollTrigger
  • View Transitions
  • React 19
  • performance

Motion e performance: GSAP, React e View Transitions

Padrões GSAP com ScrollTrigger, movimento reduzido e versões compatíveis de View Transitions: critérios e exemplos para validar LCP, INP e CLS.

Pessoa interage com a tela do celular ao lado do notebook.

Motion bom some e guia, motion ruim trava e espanta. Ou o site fica parado e não leva o olho até o botão, ou fica tão animado que o LCP estoura e o visitante sai antes do efeito terminar. Este guia mostra o meio que funciona em site de PME: 3 padrões GSAP com ScrollTrigger, 1 base global e as novidades do React 19.2, tudo com código e com o freio junto.

Uso esta stack em produção no meu portfólio, GSAP com ScrollTrigger mais Lenis e Tailwind 4, com First Load da home em torno de 118 kB (medição própria de lab, 2026).

Key Takeaways

  • Use 3 padrões e pare por aí: reveal on-enter, parallax só decorativo com scrub e pin com scrub para narrativa curta.
  • Padronize 1 ease global, stagger de 0,06 a 0,09s e clip-mask em títulos; anime só transform e opacity para não forçar layout (Google web.dev).
  • React 19.2 adicionou Activity e useEffectEvent. View Transitions depende da versão do React e da integração do framework; não é uma API estável do React 19.2.
  • Entregue versão estática real em prefers-reduced-motion e desligue pin no mobile com viewport dinâmico do iOS.

Os 3 padrões que uso em site que vende

Os três padrões que cobrem 90% das necessidades são reveal on-enter para seções, parallax só decorativo com scrub para fundo e pin com scrub para narrativa curta de 2 a 4 beats que termina em CTA. Reveal apresenta, parallax dá profundidade sem cobrar leitura, pin segura a atenção na história do produto. Qualquer coisa fora disso precisa justificar o custo antes de entrar no layout.

Gancho cliente: cada padrão termina com o botão visível. Animação que não aproxima do contato é enfeite pago com performance.

// 1. Reveal on-enter: aparece uma vez, sem replay irritante
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function initReveal() {
  gsap.utils.toArray<HTMLElement>('[data-reveal]').forEach((el) => {
    gsap.fromTo(
      el,
      { y: 28, opacity: 0 },
      {
        y: 0,
        opacity: 1,
        duration: 0.7,
        ease: 'power3.out',
        scrollTrigger: { trigger: el, start: 'top 85%', once: true },
      }
    );
  });
}
// 2. Parallax só decorativo: fundo mexe, texto fica parado e legível
export function initParallax() {
  gsap.utils.toArray<HTMLElement>('[data-parallax]').forEach((el) => {
    gsap.to(el, {
      yPercent: -12,
      ease: 'none',
      scrollTrigger: { trigger: el.parentElement, start: 'top bottom', end: 'bottom top', scrub: true },
    });
  });
}
// 3. Pin com scrub: narrativa curta presa na tela, com fim claro
export function initPinNarrative() {
  const track = document.querySelector('[data-pin-track]');
  if (!track || window.matchMedia('(max-width: 768px)').matches) return;
  gsap.to(track, {
    xPercent: -50,
    ease: 'none',
    scrollTrigger: { trigger: '[data-pin]', start: 'top top', end: '+=200%', pin: true, scrub: 1 },
  });
}

Quando não usar: parallax em texto corrido nunca, pin em página com pressa de contato nunca, e os três juntos na mesma dobra nunca. Um protagonista por tela.

Base global: 1 ease, stagger curto e clip-mask

Site com cara profissional não tem 5 easings diferentes, tem um. Defina 1 ease global (power3.out resolve quase tudo), stagger de 0,06 a 0,09s entre itens de lista ou cards para leitura em cascata sem arrastar (padrão de projeto do autor, 2026), títulos com clip-mask que revelam de baixo para cima e regra férrea de animar só transform e opacity, que não forçam recálculo de layout e protegem CLS e INP (Google web.dev, guia de animações de alta performance).

Timeline do stagger entre cards

Stagger curto em cascata: ordem sem espera.

// Base global: 1 ease, stagger e máscara de título
export const EASE_GLOBAL = 'power3.out';
export const STAGGER = 0.08;

export function initStagger() {
  gsap.utils.toArray<HTMLElement>('[data-stagger]').forEach((group) => {
    gsap.fromTo(
      group.children,
      { y: 24, opacity: 0 },
      { y: 0, opacity: 1, duration: 0.6, ease: EASE_GLOBAL, stagger: STAGGER,
        scrollTrigger: { trigger: group, start: 'top 85%', once: true } }
    );
  });
}
/* Clip-mask em títulos: revela sem empurrar layout */
.mask-titulo { overflow: hidden; display: block; }
.mask-titulo > span { display: block; transform: translateY(110%); }

Para a base que sustenta isso sem travar, veja a migração Next.js 16.

Activity e useEffectEvent no React 19.2; View Transitions em versões compatíveis

O lançamento oficial do React 19.2 inclui Activity, que preserva estado ao ocultar conteúdo, e useEffectEvent, para separar eventos da sincronização de efeitos. View Transitions não foi lançada como API estável nessa versão. O exemplo abaixo segue a documentação atual de ViewTransition: confira a versão suportada pelo seu framework antes de usá-lo. A atualização de estado precisa ocorrer em uma Transition para ativar a animação; trocar uma prop de forma síncrona não basta.

// Requer versão do React compatível com ViewTransition.
// O componente pai deve atualizar ativa dentro de startTransition.
import { ViewTransition } from 'react';

export function Abas({ ativa }: { ativa: string }) {
  return (
    <ViewTransition>
      <section key={ativa}>
        <h2>{ativa}</h2>
      </section>
    </ViewTransition>
  );
}
// Activity: esconde painel mantendo estado do formulário
import { Activity } from 'react';

export function Painel({ visivel }: { visivel: boolean }) {
  return (
    <Activity mode={visivel ? 'visible' : 'hidden'}>
      <form>...</form>
    </Activity>
  );
}
// useEffectEvent: handler estável dentro de efeito com scroll
import { useEffect, useEffectEvent } from 'react';

export function useLogScroll(ativo: boolean) {
  const onScroll = useEffectEvent(() => {
    // lê estado atual sem reinscrever o listener
    if (ativo) console.log(window.scrollY);
  });
  useEffect(() => {
    window.addEventListener('scroll', onScroll);
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
}

O que aprendi dissecando a Plantica

A Plantica, referência de site narrativo com scroll coreografado, funciona por três decisões copiáveis para orçamento de PME: camadas defasadas que se movem em velocidades diferentes para dar profundidade com poucos assets, 1 ideia por beat de scroll para ninguém ler parágrafo em movimento, e 1 truque repetido que vira identidade em vez de 10 efeitos aleatórios. Copie a estrutura, não o asset: duas camadas e um ritmo já mudam a percepção de qualidade.

Camadas defasadas em scroll narrativo estilo Plantica

Aplicação direta: hero com fundo em parallax lento mais título em reveal, seção de benefício com stagger de cards, faixa narrativa com pin curto que termina no botão de WhatsApp.

Performance e acesso: motion que não exclui nem trava

Toda animação precisa de versão estática real para quem prefere movimento reduzido, não só transição desligada com conteúdo invisível, conforme a media query prefers-reduced-motion (W3C) e o guia de acessibilidade do web.dev. No mobile, desligue pin por padrão: viewport dinâmico do iOS muda a altura com a barra do Safari e pin calculado errado gera salto e CLS. E saiba o preço de cada padrão antes de vender: reveal custa pouco, parallax moderado, pin com scrub custa caro e pede narrativa curta.

Custo relativo de performance por padrão

Custo relativo dos padrões: reveal primeiro, pin por último.

// Freio real: reduced motion vira estático, mobile sem pin
export function motionLiberado(): boolean {
  return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

export function initMotion() {
  if (!motionLiberado()) return; // conteúdo já visível no HTML/CSS base
  initReveal();
  initStagger();
  initParallax();
  initPinNarrative(); // interna já ignora mobile
}

Quadro ilustrado de animações de interface e performance

4 erros que fazem motion bonito derrubar conversão

Os quatro erros que mais vejo em site bonito que não vende: parallax em texto corrido, tudo entrando com o mesmo fade, pin longo no mobile e animar width, top ou left, que força layout a cada frame e piora CLS e INP.

Para o plano completo de site que converte, volte ao guia pilar site que vende todo dia. E proteja as métricas que o motion não pode quebrar com o checklist Core Web Vitals.

Perguntas frequentes

GSAP pesa muito no bundle?

O núcleo mais ScrollTrigger soma dezenas de kB gzip, valor que se paga quando o scroll é o vendedor, como em página de serviço ou case. Se o site só precisa de fade de entrada e transição de aba, View Transitions do React mais CSS resolvem sem a lib. No meu portfólio, GSAP entra porque o scroll conta a história até o WhatsApp, com First Load da home em torno de 118 kB (medição própria de lab, 2026).

View Transitions substituem o GSAP?

Não, elas dividem o trabalho. View Transitions cuidam de troca de estado e rota com pouco código, GSAP com ScrollTrigger cuida de scroll coreografado com scrub, pin e stagger fino. Use React para transição de UI e GSAP para narrativa de scroll, com transform e opacity nos dois para proteger as métricas.

Lenis conflita com ScrollTrigger?

Não, desde que você conecte os dois: Lenis assume o smooth scroll e avisa o ScrollTrigger a cada frame para recalcular posições. Sem essa ponte, o trigger dispara fora de hora e o pin erra a medida. Com a ponte ligada e scrub: 1 em vez de scrub seco, o movimento fica macio sem dessincronizar o marcador.

Conclusão

  • 3 padrões bastam: reveal on-enter, parallax decorativo com scrub e pin com scrub em narrativa curta.
  • Base global: 1 ease, stagger 0,06 a 0,09s, clip-mask em títulos, só transform e opacity.
  • React 19.2 trouxe Activity e useEffectEvent; use View Transitions apenas em versões compatíveis e com atualizações em Transition.
  • Freio obrigatório: versão estática real em reduced motion e pin desligado no mobile.