Pular para o conteúdo
Zumkai

Motion Design para Web: Guia Completo

Scroll, texto, imagens e vídeo: o catálogo completo de técnicas de motion para web, com implementação em Next.js e os casos em que cada uma compensa.

  • motion
  • scroll
Gradiente holográfico em azul, laranja e magenta, em movimento fluido
Sumário
  1. Índice
  2. 1. Fundamentos
  3. 2. Técnicas de Scroll Motion
  4. 3. Técnicas de Motion para Texto
  5. 4. Técnicas de Motion para Imagens
  6. 5. Técnicas de Motion para Vídeos
  7. 6. Transições de Página e Microinterações
  8. 7. Bibliotecas e Ferramentas
  9. 8. Implementação em Next.js
  10. 9. Sites de Referência e Inspiração
  11. 10. Performance e Acessibilidade
  12. 11. Roteiro de Estudo

Índice

  1. Fundamentos: os 2 tipos de animação por scroll
  2. Técnicas de Scroll Motion
  3. Técnicas de Motion para Texto
  4. Técnicas de Motion para Imagens
  5. Técnicas de Motion para Vídeos
  6. Transições de Página e Microinterações
  7. Bibliotecas e Ferramentas (o que cada uma faz)
  8. Implementação em Next.js (setup + código por técnica)
  9. Sites de Referência e Inspiração
  10. Performance e Acessibilidade
  11. Roteiro de Estudo Sugerido

1. Fundamentos

Antes de qualquer técnica, entenda a distinção mais importante do motion baseado em scroll. Praticamente tudo que você vê em sites premiados cai em uma dessas duas categorias:

1.1 Scroll-triggered (disparada por scroll)

A animação dispara uma vez quando o elemento entra no viewport e roda com duração própria (tempo). O scroll é apenas o gatilho.

  • Exemplos: fade-in de cards, texto que sobe ao aparecer, contadores animados.
  • Ferramentas típicas: whileInView (Motion), ScrollTrigger sem scrub (GSAP), IntersectionObserver, CSS animation-timeline: view() com animation-range curto.

1.2 Scroll-linked / scrubbed (vinculada ao scroll)

O progresso da animação é o progresso do scroll. Rolar para frente avança a animação, rolar para trás retrocede. Não existe duração em segundos; existe um intervalo de scroll.

  • Exemplos: parallax, barra de progresso de leitura, vídeo "scrubado" estilo Apple, seções pinadas que se transformam conforme você rola.
  • Ferramentas típicas: ScrollTrigger com scrub (GSAP), useScroll + useTransform (Motion), CSS animation-timeline: scroll().

1.3 Conceitos que você vai usar o tempo todo

ConceitoO que é
Trigger / start / endPonto do viewport onde a animação começa e termina (ex.: start: "top 80%")
ScrubVincular progresso da timeline ao scroll (com ou sem "lag" suavizado, ex.: scrub: 1)
PinFixar um elemento na tela enquanto um intervalo de scroll acontece "por baixo"
StaggerAtraso incremental entre elementos de um grupo (letras, cards, linhas)
Lerp (interpolação linear)Base do smooth scroll: a posição visual "persegue" a posição real do scroll
EasingCurva de aceleração (ease-out para entradas, ease-in-out para movimentos)
TimelineSequência orquestrada de tweens com controle de sobreposição e ordem
FLIPTécnica First-Last-Invert-Play para animar mudanças de layout com performance
Viewport amount / thresholdQuanto do elemento precisa estar visível para disparar (ex.: 30%)

2. Técnicas de Scroll Motion

2.1 Reveal on Scroll (entrada de elementos)

A técnica mais comum da web. Elementos entram com fade + translate (geralmente opacity: 0 + translateY(30-60px) → estado final) quando atingem ~70-85% do viewport.

  • Variações: slide lateral, scale-in (0.9 → 1), blur-in (filter: blur(8px) → 0), rotate sutil.
  • Boas práticas: distâncias curtas (20-60px), duração 0.5-0.9s, ease-out, disparar apenas uma vez (once: true) em conteúdo informativo.
  • Como fazer: whileInView no Motion, ScrollTrigger no GSAP, ou 100% CSS com animation-timeline: view().

2.2 Parallax

Camadas se movem em velocidades diferentes durante o scroll, criando profundidade. O fundo se move mais devagar (ou o primeiro plano mais rápido).

  • Variações: parallax de background, parallax multicamadas (foreground/midground/background), parallax de textos sobre imagens, "depth parallax" com mouse + scroll combinados.
  • Regra de ouro: deslocamentos sutis (10-30% da altura da seção). Parallax exagerado enjoa.
  • Como fazer: useScroll + useTransform (Motion), ScrollTrigger com scrub (GSAP), ou CSS puro com animation-timeline: scroll().

2.3 Pinning (seções fixas / sticky sections)

Um elemento fica preso na tela enquanto o usuário "rola através" de um intervalo virtual. Base de quase toda narrativa scrollytelling.

  • Usos: hero que se transforma, storytelling passo a passo, produto girando enquanto specs aparecem ao lado.
  • Como fazer: pin: true no ScrollTrigger (o padrão do mercado), ou position: sticky + CSS scroll-driven animations para casos simples.
  • Cuidado: pinning cria espaçadores no layout (pinSpacing); teste redimensionamento e mobile.

2.4 Scrub Animations (timeline controlada pelo scroll)

Uma timeline inteira (múltiplos elementos, sequência coreografada) avança e retrocede com o scroll. É o coração do scrollytelling moderno.

  • Exemplo clássico: seção pinada onde, conforme você rola: título sai → imagem escala → texto 2 entra → cor de fundo muda.
  • Como fazer: gsap.timeline({ scrollTrigger: { scrub: 1, pin: true } }). O valor numérico do scrub (scrub: 0.5~1.5) adiciona suavização.

2.5 Horizontal Scroll

O usuário rola verticalmente, mas o conteúdo se move horizontalmente. Ótimo para galerias, cases, timelines e portfólios.

  • Padrão de implementação: container pinado + translateX do conteúdo interno proporcional ao scroll vertical. A distância de scroll vertical "reservada" = largura horizontal a percorrer.
  • Variações: seções horizontais com snap por painel, galerias horizontais dentro de página vertical, direção invertida (right-to-left).
  • Cuidados: acessibilidade (navegação por teclado), mobile (às vezes é melhor virar scroll horizontal nativo com overflow-x + snap), e indicar visualmente que há mais conteúdo.

2.6 Smooth Scrolling (rolagem suavizada)

Substitui o scroll "seco" do navegador por uma rolagem com inércia e interpolação (lerp), dando a sensação "cinemática" dos sites de agência.

  • Ferramenta padrão em 2026: Lenis (da Darkroom Engineering, ex-Studio Freight). Leve, funciona com scroll nativo (mantém position: sticky, acessibilidade e SEO) e se integra a GSAP/Motion.
  • Alternativas: ScrollSmoother (GSAP, com efeitos de lag/velocidade por elemento), Locomotive Scroll v5 (agora construído sobre Lenis).
  • Cuidado: em touch, o padrão é não suavizar (o momentum nativo do mobile já é bom). E overflow-x: hidden no body quebra position: sticky com Lenis; use overflow-x: clip.

2.7 Scroll Progress Indicator (barra de progresso)

Barra (geralmente no topo) que preenche conforme o progresso de leitura da página ou de um artigo.

  • Como fazer: useScroll().scrollYProgressscaleX (Motion, 5 linhas), ou CSS puro com animation-timeline: scroll(root) (zero JS).

2.8 Scroll Snapping

O scroll "encaixa" em seções ou itens (fullpage sections, carrosséis).

  • Como fazer: CSS nativo scroll-snap-type + scroll-snap-align (preferível), ou snap do ScrollTrigger para encaixar em pontos da timeline.
  • Cuidado: snap fullpage agressivo frustra usuários; use proximity em vez de mandatory quando possível.

2.9 Stacked / Sticky Cards (cartas empilhadas)

Cards ficam sticky e se empilham: o card seguinte sobe por cima do anterior, que escala levemente para trás e escurece. Muito comum em seções de features/serviços.

  • Como fazer: position: sticky; top: X em cada card + scale/brightness controlados por scroll (Motion useScroll com target no container, ou ScrollTrigger, ou CSS view()).

2.10 Zoom / Scale on Scroll

Elemento (imagem, vídeo, texto gigante) escala conforme o scroll: hero que "abre" de um retângulo pequeno para fullscreen, ou imagem que dá zoom até revelar a próxima seção "dentro" dela.

  • Como fazer: pin + scrub animando scale e clip-path/border-radius.

2.11 Velocity-based Effects (efeitos por velocidade de scroll)

Elementos reagem à velocidade do scroll, não só à posição: texto que inclina (skew) quando você rola rápido, marquee que acelera e inverte direção com a direção do scroll.

  • Como fazer: useVelocity(scrollY) no Motion, ScrollTrigger.getVelocity() no GSAP, ou o callback do Lenis (que expõe velocity).

2.12 Infinite Marquee (esteira infinita)

Faixa de logos/texto/imagens rolando em loop infinito. Frequentemente combinada com velocidade do scroll (item 2.11).

  • Como fazer: duplicar o conteúdo e animar x de 0 a -50% em loop (CSS ou GSAP xPercent com wrap).

2.13 Scroll Storytelling / Scrollytelling

Combinação de tudo acima para contar uma narrativa: seções pinadas + scrub + texto sincronizado + mídia que muda por etapa. É o formato dos grandes especiais jornalísticos (NYT, Pudding.cool) e páginas de produto da Apple.

3. Técnicas de Motion para Texto

3.1 Split Text Reveal (revelação por caractere/palavra/linha)

O texto é quebrado em chars, words ou lines e cada pedaço anima com stagger. É a técnica de tipografia animada dos sites premiados.

  • Variações:
  • Por linha com máscara: cada linha sobe de dentro de um overflow: hidden (efeito "cortina"). O visual mais elegante e o mais usado.
  • Por palavra: fade/slide com stagger de ~0.05s.
  • Por caractere: entradas dramáticas de headlines (stagger 0.02-0.04s).
  • Ferramentas: SplitText do GSAP (gratuito desde 2025, lida com acessibilidade e quebra responsiva de linhas), splitText do Motion+, ou split manual em React (map de palavras → motion.span).
  • Cuidado: splits quebram com fontes que ainda não carregaram (use document.fonts.ready) e precisam de aria-label no elemento pai para leitores de tela (SplitText já resolve isso).

3.2 Text Reveal on Scroll (destaque progressivo)

Parágrafo longo em que as palavras "acendem" (de cinza para branco/preto) conforme o scroll avança, uma a uma. Popularizado por sites de agência e landing pages de IA.

  • Como fazer: split em palavras + opacidade de cada palavra mapeada a uma fatia do scrollYProgress, geralmente com a seção pinada.

3.3 Typewriter (máquina de escrever)

Caracteres aparecem em sequência, com cursor piscando. Bom para heros de produto dev/IA.

  • Como fazer: bibliotecas como Typed.js, TextPlugin/SplitText do GSAP, ou implementação própria com setInterval/RAF.

3.4 Scramble / Decode Text

Caracteres aleatórios "resolvem" no texto final (efeito Matrix/hacker).

  • Como fazer: ScrambleTextPlugin do GSAP (gratuito), ou implementação própria trocando chars aleatórios a cada frame.

3.5 Kinetic Typography (tipografia cinética)

Texto gigante como elemento gráfico principal: palavras que se movem em direções opostas no scroll, títulos que atravessam a tela, texto em círculo girando, texto que distorce.

  • Variações: linhas alternadas movendo para esquerda/direita com scrub; texto em path SVG (círculo/curva) girando com o scroll (MotionPathPlugin); skew por velocidade.

3.6 Variable Font Animation

Animação dos eixos de uma fonte variável (font-variation-settings: weight, width, slant) por scroll, hover ou proximidade do mouse.

  • Como fazer: CSS transitions/animations no font-variation-settings, ou GSAP animando o valor. Efeito sofisticado e barato em performance quando usado com moderação.

3.7 Text Mask / Clip Reveal

Texto revelado por clip-path ou background-clip: text (gradiente que se move dentro do texto, imagem/vídeo "dentro" das letras).

  • Como fazer: background-clip: text + background-position animado; ou vídeo com mask/SVG <clipPath> em forma de texto.

Underline animado (scaleX da esquerda → direita), troca de caracteres no hover (rolagem vertical de letras duplicadas), preenchimento de fundo, magnetismo.

  • Como fazer: CSS para underline/preenchimento; GSAP/Motion para roll de letras (duas cópias do texto num overflow: hidden, translateY no hover).

3.9 Counter / Number Ticker

Números que contam de 0 até o valor final ao entrar no viewport (métricas, estatísticas).

  • Como fazer: useMotionValue + animate() no Motion, ou tween de um objeto { value: 0 } no GSAP com snap: 1, disparado por whileInView/ScrollTrigger.

4. Técnicas de Motion para Imagens

4.1 Image Reveal (revelação de imagem)

A imagem entra com efeito de máscara em vez de simples fade:

  • Clip-path reveal: clip-path: inset(100% 0 0 0)inset(0) (revela de baixo para cima; qualquer direção funciona).
  • Curtain reveal: um painel colorido cobre a imagem e desliza para fora.
  • Scale + parent overflow: imagem em scale(1.3) dentro de container overflow: hidden, ambos animando em direções opostas (o clássico "reveal de agência").

4.2 Parallax de Imagem Interna

A imagem se move dentro de um frame com overflow: hidden conforme o scroll (imagem 120% da altura do container, translateY de -10% a 10%). Dá vida a qualquer grid de fotos.

4.3 Ken Burns / Slow Zoom

Zoom lento e contínuo (scale 1 → 1.08 em 8-20s) em imagens de hero/background. Barato e eficaz.

Múltiplas imagens em camadas escalando em velocidades diferentes numa seção pinada, criando efeito de "mergulho" (popularizado por tutoriais do Olivier Larose).

4.5 Hover Distortion (WebGL)

Distorção líquida/ondulada da imagem no hover, transições com displacement maps entre imagens. O visual "high-end" de portfólios criativos.

  • Como fazer: shaders WebGL via OGL, Three.js/React Three Fiber, ou bibliotecas prontas (curtains.js, hover-effect.js). Custo: complexidade + bundle.

Rastro de imagens seguindo o cursor (cada movimento "carimba" uma imagem que escala e some). Efeito de hero de sites criativos.

Entradas coreografadas de grids: stagger em diagonal, colunas subindo em velocidades diferentes (coluna 1 sobe, coluna 2 desce no scroll), reordenação animada com FLIP.

  • Como fazer: stagger do GSAP/Motion; Flip plugin do GSAP ou prop layout do Motion para reordenações.

4.8 Image Sequence on Scroll (sprite de frames)

Sequência de dezenas/centenas de frames (JPEG/WebP) desenhados em <canvas> conforme o scroll, simulando um vídeo scrubado com controle perfeito. É como a Apple faz as páginas do AirPods/iPhone.

  • Como fazer: pré-carregar frames → ScrollTrigger/useScroll mapeia progresso → índice do frame → drawImage no canvas. (Código na seção 8.)

4.9 Comparação / Before-After Slider

Duas imagens sobrepostas com divisor arrastável ou controlado por scroll (clip-path na imagem de cima).

5. Técnicas de Motion para Vídeos

5.1 Video Scrubbing on Scroll

O currentTime do vídeo é controlado pelo progresso do scroll numa seção pinada. O usuário literalmente "toca" o vídeo rolando.

  • Requisitos técnicos críticos: o vídeo precisa ser encodado com keyframes frequentes (idealmente todo frame, ex.: ffmpeg -g 1), sem áudio, curto e otimizado. Sem isso, o scrub trava.
  • Alternativa mais confiável: image sequence em canvas (4.8), especialmente para iOS/Safari.

5.2 Autoplay on View (play/pause por visibilidade)

Vídeo dá play ao entrar no viewport e pausa ao sair (economia de bateria/dados e foco de atenção).

  • Como fazer: IntersectionObserver (ou useInView do Motion) chamando video.play()/pause(). Vídeo precisa ser muted + playsInline para autoplay funcionar.

5.3 Video Reveal / Expand on Scroll

Vídeo começa pequeno (ou com border-radius grande) e expande para fullscreen conforme o scroll, geralmente pinado (padrão de homepage de SaaS moderno).

5.4 Background Video com Overlay Animado

Vídeo de fundo (muted, loop) com camadas de texto/gradiente animando por cima, com parallax leve entre as camadas.

5.5 Video Mask / Video em Texto

Vídeo visível apenas dentro de uma forma ou de tipografia (SVG <clipPath>/mask ou background-clip), com a máscara animando (escala/posição) no scroll.

5.6 Hover Preview

Thumbnail estática que vira vídeo em preview no hover (cards de portfólio/streaming). onMouseEnter → play; onMouseLeave → pause + reset.

5.7 Transições de Vídeo entre Seções

Vídeo pinado que troca de "capítulo" (jump para timestamps ou crossfade entre vídeos) conforme etapas de texto passam por ele. Base do scrollytelling com vídeo.

6. Transições de Página e Microinterações

6.1 Page Transitions (transições entre rotas)

  • View Transitions API (nativa): o navegador tira "snapshot" do estado antigo e anima para o novo. Com view-transition-name, elementos compartilhados (ex.: thumbnail → imagem grande) fazem morph automático entre páginas. Suporte amplo em 2026 (Chrome/Edge desde 111, Safari 18+, Firefox 129+ para same-document). No Next.js há integração experimental com o componente <ViewTransition> do React 19.2 (seção 8.9).
  • AnimatePresence (Motion): transições de entrada/saída controladas por JS, com mais liberdade criativa (curtains, wipes, overlays com contagem/logotipo).
  • Curtain/Overlay transition: painel cobre a tela na saída e revela a nova página (padrão de sites de agência; em stacks não-React, o barba.js é a referência).

6.2 Preloader / Intro Animation

Tela de abertura com contador de porcentagem, logo animado ou palavras trocando, terminando num reveal da home. Use com parcimônia (custa tempo do usuário).

6.3 Microinterações

  • Magnetic buttons: botão "atraído" pelo cursor dentro de um raio (lerp da posição do mouse).
  • Custom cursor: cursor substituído por um círculo com lag (lerp), que reage a hovers (escala, texto "view", blend mode).
  • Hover states ricos: cards com tilt 3D (rotateX/rotateY pelo mouse), brilho seguindo o cursor, ícones com micro-animações.
  • Feedback de UI: botões com spring no clique, toggles animados, skeleton → conteúdo com crossfade, toasts com spring.

7. Bibliotecas e Ferramentas

7.1 As essenciais (o "stack padrão" de 2026)

FerramentaO que fazQuando usar
GSAP 3.13+A plataforma de animação JS mais completa. Timelines, easing avançado, e plugins: ScrollTrigger (tudo de scroll: trigger, scrub, pin, snap), ScrollSmoother, SplitText, Flip, Observer, Draggable, MotionPath, MorphSVG, DrawSVG, ScrambleText. 100% gratuito desde abril/2025 (após aquisição pela Webflow), incluindo todos os plugins antes pagos. Hook oficial useGSAP() via @gsap/react resolve o ciclo de vida no React.Scrollytelling complexo, pinning, horizontal scroll, animação de texto profissional, SVG. É o padrão da indústria criativa (maioria dos sites premiados no Awwwards).
Motion v12+ (ex-Framer Motion)Biblioteca de animação declarativa. No React: motion/react (props animate, whileInView, whileHover), AnimatePresence (animações de saída/unmount), layout (FLIP automático), hooks useScroll, useTransform, useSpring, useVelocity, useInView. Motor híbrido: usa WAAPI/ScrollTimeline nativos quando possível (animações fora da main thread) com fallback JS para springs/gestos. Renomeada de Framer Motion para Motion (projeto independente, motion.dev); import mudou de framer-motion para motion/react.Animações de UI em React/Next.js: entradas, hover, layout animations, exit animations, scroll-linked simples/médio. A DX mais natural para componentes React.
Lenis 1.xSmooth scroll com lerp sobre o scroll nativo (preserva sticky, acessibilidade, SEO). Integração React via lenis/react (<ReactLenis root>). Sincroniza com ScrollTrigger e Motion. Atenção: os pacotes antigos @studio-freight/react-lenis e @studio-freight/lenis foram descontinuados; use o pacote lenis.Qualquer site que queira o "feel" premium de agência. Combina com GSAP ou Motion.
CSS Scroll-Driven Animations (nativo)animation-timeline: scroll() (progresso do container) e view() (visibilidade do elemento) + animation-range. Roda na thread do compositor: performance imbatível, zero JS. Suporte em 2026: Chrome/Edge 115+, Firefox 132+, Safari 18+/26 (~85-90% global). Use @supports (animation-timeline: scroll()) para progressive enhancement.Reveals, parallax simples, barras de progresso. Não cobre pin complexo, snap de timeline nem coreografia avançada (aí é GSAP).
View Transitions API (nativa) + React <ViewTransition>Transições entre estados do DOM e entre páginas, com morph de elementos compartilhados via view-transition-name. Integração experimental no Next.js (experimental.viewTransition).Transições de rota e de estado (listas, filtros, thumbnail→detalhe) sem biblioteca.

7.2 Complementares e especializadas

FerramentaO que faz
React Three Fiber + dreiThree.js declarativo em React. Cenas 3D, shaders, WebGL. drei traz helpers (ScrollControls, câmeras, materiais). Para heros 3D e experiências imersivas.
OGL / curtains.jsWebGL minimalista para efeitos de imagem (distorção em hover, displacement) sem o peso do Three.js.
anime.js v4Biblioteca JS leve e elegante para animações de propriedades, SVG e timelines. Alternativa menor ao GSAP para casos simples/médios.
React SpringAnimações baseadas em física de mola no React. Alternativa ao Motion com filosofia "tudo é spring".
Lottie (lottie-react) / RiveReprodução de animações vetoriais criadas no After Effects (Lottie) ou no editor Rive (interativas, máquina de estados, mais leves). Ícones, ilustrações e mascotes animados.
Swiper / Embla CarouselCarrosséis e sliders com gestos. Embla é headless e leve, ótimo com React.
AOS (Animate On Scroll)Reveals via atributos data-aos. Simples, mas em 2026 é geralmente substituível por CSS scroll-driven animations ou Motion.
Theatre.jsEditor visual de timelines para coreografias complexas (inclusive 3D) com interface de keyframes.
barba.jsTransições de página em sites multi-page não-React.
Locomotive Scroll v5Smooth scroll + detecção de elementos; a v5 é construída sobre Lenis.
tsParticles / three-globe etc.Partículas e efeitos decorativos.

7.3 Como escolher (regra prática)

  1. Só entradas e reveals simples? CSS scroll-driven animations (com fallback @supports) ou Motion whileInView.
  2. App React com UI animada (modais, listas, layout)? Motion.
  3. Scrollytelling, pin, horizontal scroll, split text? GSAP + ScrollTrigger (+ Lenis).
  4. Feel premium de agência? Lenis + GSAP (o combo mais usado em sites Awwwards).
  5. Transição de rota? View Transitions API; efeitos artísticos de saída, AnimatePresence.
  6. Imagens com distorção/3D? R3F ou OGL.
  7. Motion e GSAP convivem bem no mesmo projeto: Motion para UI, GSAP para as seções cinematográficas.

8. Implementação em Next.js

Tudo abaixo assume Next.js 15/16 (App Router) + TypeScript + React 19.

8.0 Regras de ouro no App Router

  1. Animação = client component. Todo componente com GSAP/Motion/Lenis precisa de "use client" no topo. Mantenha as páginas como Server Components e isole a animação em componentes folha.
  2. Nunca toque em window/document no render. Efeitos só em useEffect/useGSAP.
  3. Cleanup obrigatório. ScrollTriggers e listeners precisam ser destruídos no unmount (troca de rota no App Router não recarrega a página). O useGSAP faz isso automaticamente.
  4. Fontes antes de SplitText. Aguarde document.fonts.ready antes de splitar texto.
  5. Respeite prefers-reduced-motion (seção 10).
  6. Hydration: para evitar "flash" de conteúdo no estado inicial errado, defina o estado inicial via CSS (ex.: classe com opacity: 0) ou use initial do Motion, e evite divergência servidor/cliente.

8.1 Setup de pacotes

bash
npm i gsap @gsap/react lenis motion

8.2 Smooth scroll global com Lenis

tsx
// app/providers/smooth-scroll.tsx
"use client";

import { ReactLenis } from "lenis/react";

export function SmoothScroll({ children }: { children: React.ReactNode }) {
  return (
    <ReactLenis
      root
      options={{
        duration: 1.1,
        easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
        smoothWheel: true,
        syncTouch: false, // touch mantém o momentum nativo
      }}
    >
      {children}
    </ReactLenis>
  );
}
tsx
// app/layout.tsx
import { SmoothScroll } from "./providers/smooth-scroll";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <body>
        <SmoothScroll>{children}</SmoothScroll>
      </body>
    </html>
  );
}

Integração Lenis + ScrollTrigger (necessária quando usar os dois):

tsx
// app/providers/smooth-scroll.tsx (versão integrada com GSAP)
"use client";

import { ReactLenis, useLenis } from "lenis/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useEffect } from "react";

gsap.registerPlugin(ScrollTrigger);

function GsapSync() {
  const lenis = useLenis(() => ScrollTrigger.update());

  useEffect(() => {
    if (!lenis) return;
    // GSAP passa a comandar o RAF do Lenis
    const raf = (time: number) => lenis.raf(time * 1000);
    gsap.ticker.add(raf);
    gsap.ticker.lagSmoothing(0);
    return () => gsap.ticker.remove(raf);
  }, [lenis]);

  return null;
}

export function SmoothScroll({ children }: { children: React.ReactNode }) {
  return (
    <ReactLenis root options={{ autoRaf: false, duration: 1.1 }}>
      <GsapSync />
      {children}
    </ReactLenis>
  );
}

8.3 Reveal on scroll

Com Motion (o jeito mais rápido):

tsx
"use client";
import { motion } from "motion/react";

export function Reveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.3 }}
      transition={{ duration: 0.7, ease: [0.21, 0.47, 0.32, 0.98] }}
    >
      {children}
    </motion.div>
  );
}

Com GSAP + useGSAP (batch para vários elementos):

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger, useGSAP);

export function RevealSection() {
  const container = useRef<HTMLElement>(null);

  useGSAP(
    () => {
      gsap.from(".card", {
        y: 60,
        opacity: 0,
        duration: 0.8,
        ease: "power3.out",
        stagger: 0.12,
        scrollTrigger: {
          trigger: container.current,
          start: "top 75%",
          once: true,
        },
      });
    },
    { scope: container } // seletores ficam restritos ao container + cleanup automático
  );

  return (
    <section ref={container}>
      <div className="card">…</div>
      <div className="card">…</div>
      <div className="card">…</div>
    </section>
  );
}

100% CSS (progressive enhancement, zero JS):

css
@supports (animation-timeline: view()) {
  .reveal {
    animation: reveal-up linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 60%;
  }
  @keyframes reveal-up {
    from { opacity: 0; transform: translateY(40px); }
    to   { opacity: 1; transform: none; }
  }
}

8.4 Parallax

Com Motion (useScroll + useTransform):

tsx
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";

export function ParallaxImage({ src }: { src: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"], // do momento em que entra até sair do viewport
  });
  const y = useTransform(scrollYProgress, [0, 1], ["-12%", "12%"]);

  return (
    <div ref={ref} style={{ overflow: "hidden", borderRadius: 12 }}>
      <motion.img
        src={src}
        style={{ y, scale: 1.25, width: "100%", display: "block" }}
        alt=""
      />
    </div>
  );
}

Com GSAP:

tsx
useGSAP(() => {
  gsap.to(".parallax-img", {
    yPercent: 20,
    ease: "none",
    scrollTrigger: {
      trigger: ".parallax-wrap",
      start: "top bottom",
      end: "bottom top",
      scrub: true,
    },
  });
}, { scope: container });

8.5 Seção pinada com timeline scrubada (scrollytelling)

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger, useGSAP);

export function PinnedStory() {
  const container = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: container.current,
          start: "top top",
          end: "+=3000",   // 3000px de scroll "virtual" para a narrativa
          scrub: 1,        // suavização de 1s
          pin: true,
        },
      });

      tl.to(".step-1", { opacity: 0, y: -40 })
        .fromTo(".hero-img", { scale: 0.6 }, { scale: 1 }, "<")
        .fromTo(".step-2", { opacity: 0, y: 40 }, { opacity: 1, y: 0 })
        .to(".bg", { backgroundColor: "#0e0e10" }, "<");
    },
    { scope: container }
  );

  return (
    <div ref={container} className="bg" style={{ height: "100vh", position: "relative" }}>
      <p className="step-1">Primeiro ato…</p>
      <img className="hero-img" src="/produto.png" alt="" />
      <p className="step-2" style={{ opacity: 0 }}>Segundo ato…</p>
    </div>
  );
}

8.6 Horizontal scroll

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger, useGSAP);

export function HorizontalGallery() {
  const container = useRef<HTMLDivElement>(null);
  const track = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const getDistance = () =>
        (track.current?.scrollWidth ?? 0) - window.innerWidth;

      gsap.to(track.current, {
        x: () => -getDistance(),
        ease: "none",
        scrollTrigger: {
          trigger: container.current,
          start: "top top",
          end: () => `+=${getDistance()}`, // scroll vertical = distância horizontal
          scrub: 1,
          pin: true,
          invalidateOnRefresh: true, // recalcula em resize
        },
      });
    },
    { scope: container }
  );

  return (
    <section ref={container} style={{ overflow: "hidden" }}>
      <div ref={track} style={{ display: "flex", gap: 24, width: "max-content" }}>
        {[1, 2, 3, 4, 5, 6].map((i) => (
          <div key={i} style={{ width: "60vw", height: "70vh", flexShrink: 0 }}>
            Painel {i}
          </div>
        ))}
      </div>
    </section>
  );
}

8.7 Split text reveal (SplitText do GSAP)

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";

gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP);

export function AnimatedHeadline({ text }: { text: string }) {
  const ref = useRef<HTMLHeadingElement>(null);

  useGSAP(async () => {
    await document.fonts.ready; // evita split com fonte errada

    const split = SplitText.create(ref.current, {
      type: "lines,words",
      mask: "lines", // cria o overflow:hidden por linha (efeito cortina)
      autoSplit: true, // re-splita em resize/carregamento de fonte
    });

    gsap.from(split.words, {
      yPercent: 110,
      duration: 0.9,
      ease: "power4.out",
      stagger: 0.04,
      scrollTrigger: { trigger: ref.current, start: "top 80%", once: true },
    });
  });

  return <h1 ref={ref}>{text}</h1>;
}

Versão sem GSAP (split manual + Motion):

tsx
"use client";
import { motion } from "motion/react";

export function WordReveal({ text }: { text: string }) {
  return (
    <h2 aria-label={text}>
      {text.split(" ").map((word, i) => (
        <span key={i} style={{ display: "inline-block", overflow: "hidden" }} aria-hidden>
          <motion.span
            style={{ display: "inline-block", marginRight: "0.3em" }}
            initial={{ y: "110%" }}
            whileInView={{ y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, ease: "easeOut", delay: i * 0.04 }}
          >
            {word}
          </motion.span>
        </span>
      ))}
    </h2>
  );
}

8.8 Barra de progresso de leitura (Motion, 10 linhas)

tsx
"use client";
import { motion, useScroll, useSpring } from "motion/react";

export function ReadingProgress() {
  const { scrollYProgress } = useScroll();
  const scaleX = useSpring(scrollYProgress, { stiffness: 120, damping: 30 });
  return (
    <motion.div
      style={{
        scaleX,
        transformOrigin: "0 0",
        position: "fixed",
        inset: "0 0 auto 0",
        height: 4,
        background: "#6366f1",
        zIndex: 50,
      }}
    />
  );
}

8.9 Transições de página (View Transitions no Next.js 16)

ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    viewTransition: true, // experimental — valide antes de usar em produção
  },
};
export default nextConfig;
tsx
// app/template.tsx — anima cada navegação de rota
"use client";
import { unstable_ViewTransition as ViewTransition } from "react";

export default function Template({ children }: { children: React.ReactNode }) {
  return <ViewTransition>{children}</ViewTransition>;
}
css
/* globals.css — customiza a transição padrão */
::view-transition-old(root) {
  animation: fade-out 0.25s ease both;
}
::view-transition-new(root) {
  animation: fade-in 0.35s ease 0.05s both;
}
@keyframes fade-out { to { opacity: 0; transform: translateY(-8px); } }
@keyframes fade-in { from { opacity: 0; transform: translateY(8px); } }

Para o efeito thumbnail → detalhe (shared element), dê o mesmo view-transition-name ao elemento nas duas rotas:

css
.product-card img { view-transition-name: var(--vt-name); }

8.10 Vídeo com autoplay por visibilidade

tsx
"use client";
import { useEffect, useRef } from "react";

export function AutoPlayVideo({ src }: { src: string }) {
  const ref = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    const video = ref.current;
    if (!video) return;
    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) video.play().catch(() => {});
        else video.pause();
      },
      { threshold: 0.5 }
    );
    io.observe(video);
    return () => io.disconnect();
  }, []);

  return <video ref={ref} src={src} muted loop playsInline preload="metadata" />;
}

8.11 Image sequence scrubada (estilo Apple)

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger, useGSAP);

const FRAME_COUNT = 120;
const frameSrc = (i: number) =>
  `/sequence/frame-${String(i + 1).padStart(4, "0")}.webp`;

export function ScrollSequence() {
  const container = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useGSAP(
    () => {
      const canvas = canvasRef.current!;
      const ctx = canvas.getContext("2d")!;
      canvas.width = 1600;
      canvas.height = 900;

      const images: HTMLImageElement[] = [];
      const state = { frame: 0 };

      for (let i = 0; i < FRAME_COUNT; i++) {
        const img = new Image();
        img.src = frameSrc(i);
        images.push(img);
      }

      const render = () => {
        const img = images[Math.round(state.frame)];
        if (img?.complete) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        }
      };

      images[0].onload = render;

      gsap.to(state, {
        frame: FRAME_COUNT - 1,
        snap: "frame",
        ease: "none",
        onUpdate: render,
        scrollTrigger: {
          trigger: container.current,
          start: "top top",
          end: "+=2500",
          scrub: 0.5,
          pin: true,
        },
      });
    },
    { scope: container }
  );

  return (
    <div ref={container} style={{ height: "100vh" }}>
      <canvas ref={canvasRef} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
    </div>
  );
}

8.12 Stacked cards (Motion + sticky)

tsx
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform, MotionValue } from "motion/react";

function Card({ i, total, progress, children }: {
  i: number; total: number; progress: MotionValue<number>; children: React.ReactNode;
}) {
  const start = i / total;
  const scale = useTransform(progress, [start, 1], [1, 1 - (total - i) * 0.04]);

  return (
    <motion.div
      style={{
        scale,
        position: "sticky",
        top: `calc(8vh + ${i * 24}px)`,
        transformOrigin: "top center",
      }}
    >
      {children}
    </motion.div>
  );
}

export function StackedCards({ items }: { items: string[] }) {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end end"] });

  return (
    <div ref={ref}>
      {items.map((item, i) => (
        <Card key={i} i={i} total={items.length} progress={scrollYProgress}>
          <div style={{ height: "70vh", borderRadius: 16, background: "#18181b" }}>{item}</div>
        </Card>
      ))}
    </div>
  );
}

8.13 Evitando os erros clássicos no Next.js

ErroCausaCorreção
window is not definedGSAP/Lenis rodando no servidor"use client" + lógica dentro de useEffect/useGSAP
Animações "fantasma" após trocar de rotaScrollTriggers antigos não destruídosuseGSAP com scope (cleanup automático) ou ScrollTrigger.getAll().forEach(t => t.kill()) no cleanup
Posições erradas de triggerImagens/fonts carregando depois do cálculoScrollTrigger.refresh() após load; dimensões fixas nas imagens (next/image com width/height)
Flash de conteúdo antes do estado inicialEstado inicial aplicado só via JSEstado inicial no CSS ou prop initial do Motion
Sticky quebrado com Lenisoverflow-x: hidden no bodyusar overflow-x: clip
Scrub de vídeo travandoEncode sem keyframes frequentesre-encodar (-g 1) ou migrar para image sequence
Bundle infladoImportar plugins não usados / lib inteiraimportar só o necessário; LazyMotion no Motion; dynamic import para seções pesadas (R3F)

9. Sites de Referência e Inspiração

9.1 Galerias de premiação e curadoria (o essencial)

SiteO que encontrar
Awwwards (awwwards.com)A principal premiação de web design criativo. Filtros por tecnologia (GSAP, Three.js) e por estilo. O "Site of the Day" é a régua do mercado.
Godly (godly.website)Curadoria enxuta e moderna, forte em motion e landing pages. Cada entrada tem vídeo de preview.
FWA (thefwa.com)Premiação focada em experiências imersivas e experimentais (WebGL pesado).
CSS Design Awards (cssdesignawards.com)Premiação alternativa ao Awwwards, com jurados da indústria.
SiteInspire (siteinspire.com)Curadoria clássica com filtros por estilo/segmento.
Httpster (httpster.net)Curadoria com personalidade, foco em tipografia e sites ousados.
Minimal Gallery (minimal.gallery)Sites minimalistas bem executados (bom contraponto: motion contido e elegante).
Land-book (land-book.com) e Curated.designLanding pages e sites de produto, ótimos para referência comercial/SaaS.
Savee (savee.it)Moodboard colaborativo com muito material de motion e direção de arte.
Dark Mode Design, Footer.design, Seesaw (seesaw.website)Curadorias de nicho úteis para detalhes específicos.

9.2 Referência técnica com código

SiteO que encontrar
Codrops (tympanus.net/codrops)Tutoriais e demos de efeitos avançados (scroll, WebGL, tipografia) com código-fonte. A melhor escola prática de motion web.
CodePen (codepen.io)Milhares de demos de ScrollTrigger, split text, parallax. Busque por "ScrollTrigger", "Lenis", "scroll-driven".
Osmo (osmo.supply)Biblioteca de componentes/efeitos de motion prontos criada por devs premiados no Awwwards.
GSAP Showcase e Demos (gsap.com/showcase)Sites reais feitos com GSAP + coleção oficial de demos por plugin.
Motion Examples (motion.dev)400+ exemplos oficiais copy-paste do Motion.
Olivier Larose (blog.olivierlarose.com)Tutoriais em vídeo + código de efeitos de sites premiados em Next.js (zoom parallax, text reveal, curved menus).
Frontend Horse (frontend.horse) e Codrops CollectiveAnálises de como efeitos de sites premiados foram construídos.
scroll-driven-animations.styleSite de referência oficial (Bramus/Chrome) com demos e ferramentas de CSS scroll-driven animations.

9.3 Sites de produto/estúdio para estudar (padrões reais)

  • Apple.com (páginas de produto): a referência de image sequence scrubada e pinning narrativo.
  • Linear.app, Vercel.com, Resend.com, Stripe.com: motion contido e funcional de produto SaaS.
  • Estúdios premiados: Locomotive (locomotive.ca), Obys (obys.agency), Basement Studio (basement.studio), Unseen Studio, Active Theory, Lusion (lusion.co), Darkroom Engineering (darkroom.engineering). Analise com DevTools: quase sempre há Lenis + GSAP por baixo.

10. Performance e Acessibilidade

10.1 Performance

  1. Anime apenas transform e opacity. São compostas na GPU sem reflow/repaint. Evite animar width, height, top, left, margin, box-shadow (anime um pseudo-elemento com opacity para sombras).
  2. Prefira o compositor. CSS scroll-driven animations e o motor híbrido do Motion (WAAPI/ScrollTimeline) rodam fora da main thread e permanecem fluidos mesmo com JS pesado.
  3. will-change com moderação. Aplique só no que vai animar e remova depois; em excesso, consome memória.
  4. Nunca use listeners de scroll crus para animação. Use ScrollTrigger/useScroll (que já otimizam com RAF) ou CSS nativo.
  5. Meça: DevTools > Performance (procure long tasks e jank), FPS meter, e teste em um Android intermediário, não só no seu desktop.
  6. Imagens/vídeos: next/image, WebP/AVIF, preload inteligente das sequences, poster em vídeos.
  7. Code splitting: next/dynamic para seções WebGL/3D; LazyMotion + m component para reduzir o bundle do Motion.

10.2 Acessibilidade

  1. prefers-reduced-motion é obrigatório. Usuários com sensibilidade vestibular podem passar mal com parallax/zoom.
css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
tsx
// Em React (Motion)
import { useReducedMotion } from "motion/react";
const reduce = useReducedMotion();
// use `reduce` para trocar slide/parallax por um simples fade (ou nada)

No GSAP, use gsap.matchMedia() com a condição (prefers-reduced-motion: reduce) para registrar versões alternativas.

  1. Conteúdo não pode depender da animação. Sem JS (ou com motion reduzido), tudo precisa estar legível.
  2. Texto splitado precisa de aria-label no elemento pai (spans soltos viram ruído no leitor de tela). O SplitText do GSAP moderno já cuida disso.
  3. Horizontal scroll e pin: garanta navegação por teclado e não sequestre o scroll além do necessário.
  4. Autoplay de vídeo sempre muted, e idealmente com controle de pausa visível para vídeos longos.

11. Roteiro de Estudo

Ordem sugerida, do fundamento ao avançado:

  1. Semana 1-2 — Fundamentos: CSS transitions/animations, easing, transform/opacity, IntersectionObserver. Implemente reveals com CSS scroll-driven animations puras.
  2. Semana 3-4 — Motion: whileInView, variants + stagger, AnimatePresence, layout, depois useScroll + useTransform (progress bar, parallax, stacked cards).
  3. Semana 5-7 — GSAP: tweens, timelines, easing; depois ScrollTrigger a fundo (trigger, scrub, pin, snap, matchMedia). Reconstrua: horizontal scroll, seção pinada narrativa, image sequence.
  4. Semana 8 — Texto: SplitText (linhas com máscara, chars com stagger), text reveal progressivo, marquee com velocidade.
  5. Semana 9 — Integração: Lenis + GSAP + Next.js App Router num projeto real (portfólio/landing). Adicione View Transitions.
  6. Contínuo: escolha 1 site do Awwwards/Godly por semana e reconstrua 1 efeito dele. É o método que mais acelera o aprendizado (Codrops e Olivier Larose ajudam muito aqui).

Documento gerado em 01/08/2026. Versões de referência: GSAP 3.13+ (gratuito, todos os plugins), Motion v12+ (motion/react), Lenis 1.3+ (lenis/react), Next.js 15/16 (App Router), React 19.2.

Leia também