Motions premium em Next.js: os 18 padrões do site da Plantica, passo a passo
O scroll vira CSS custom property e o CSS faz o resto: os 18 padrões de motion do site da Plantica em Next.js, com o prompt de implementação pronto de cada um.
- motion
- gsap

Sumário
- A filosofia que muda tudo: CSS-first, JS-orquestrado
- A stack e o papel de cada peça
- O mapa dos 18 motions
- Passo 1 — A fundação: infraestrutura de animação
- Passo 2 — O motor: scroll vira CSS custom property (Motion 4)
- Passo 3 — A assinatura tipográfica: texto clip/split (Motion 3)
- Passo 4 — Estados globais: primeira carga e transições de página
- Passo 5 — O chrome: menu card 3D, botões e underlines
- Passo 6 — O cartão de visitas: hero collage com zoom progressivo (Motion 5)
- Passo 7 — Seções em série: arquivo, painéis sticky e vídeo-íris
- Passo 8 — Polish de desktop: cursor contextual e preview flutuante
- Passo 9 — A página de detalhe: split-scroll, zoom FLIP e relacionados
- Passo 10 — Scroll infinito nas galerias (Motion 6)
- Passo 11 — O fechamento: sprites decorativos e o rodapé em três atos
- As 15 armadilhas que vão te morder (leia antes de codar)
- Por onde começar hoje
Todo desenvolvedor já viu um daqueles sites de estúdio criativo que parecem de outro campeonato: o texto entra letra por letra, o hero cresce até engolir a tela, o rodapé é revelado como cortina, o cursor conta o que cada área faz. E a pergunta que fica é sempre a mesma: como isso é feito de verdade — e como eu reproduzo num projeto Next.js sem virar um Frankenstein de bibliotecas?
Este guia responde com engenharia reversa de um caso concreto: o site da Plantica (plantica.net), o estúdio criativo floral japonês fundado por Takashi Kimura. "Plantica is a flower creative studio", anuncia o hero — e por trás da aparência delicada há um build customizado exemplar: GSAP + ScrollTrigger + Lenis + barba.js + WebGL sobre um CMS headless. Os 18 padrões abaixo foram catalogados auditando esse site, mas o material é agnóstico de conteúdo: nomes, textos e imagens são placeholders parametrizáveis. O que importa — e o que você leva daqui — é o padrão de movimento, a técnica por trás e um prompt de implementação pronto para colar no seu agente de IA ou usar como especificação.
A ordem dos passos não é a ordem do catálogo: é a ordem de construção. Cada passo assenta a base do seguinte, então dá para seguir o guia como um roteiro de projeto, do zero ao site completo.
A filosofia que muda tudo: CSS-first, JS-orquestrado
Antes de qualquer linha de código, entenda a decisão de arquitetura que sustenta o site auditado — porque ela é o oposto do que a maioria faz.
A abordagem comum é tweenar tudo via JavaScript: cada elemento animado vira um tween GSAP ou um componente Framer Motion, e o JS recalcula valores a cada frame. Funciona, mas escala mal: dezenas de tweens rodando no main thread, cleanup frágil, e qualquer refactor de layout quebra timings.
O site auditado faz diferente. O JavaScript quase não anima nada diretamente. Ele tem só dois empregos:
- Escrever o progresso do scroll em CSS custom properties (
--p1e--p2), via um único padrão de ScrollTrigger com scrub. - Alternar classes de estado no
<html>:is-loaded,is-leaving,is-menu-opened,data-shown.
Quem anima de verdade são transitions e calc() no CSS, rodando no compositor. O resultado: performance de outra categoria (nada de layout thrashing por frame), manutenção trivial (os motions viram declarações no CSS e atributos no markup) e um mapa mental simples — JS mede e orquestra, CSS move.
A stack e o papel de cada peça
| Biblioteca | Papel no projeto | Por quê |
|---|---|---|
| GSAP 3.13+ e @gsap/react | Motor de progresso de scroll (ScrollTrigger scrub → CSS vars) e orquestrações pontuais | GSAP é 100% gratuito, incluindo todos os plugins, desde a aquisição pela Webflow. Aqui ele é usado de forma minimalista: uma factory de ScrollTrigger para o site inteiro |
| ScrollTrigger (plugin GSAP) | Escrever --p1 e --p2 nos elementos com data-start e data-end | scrub curtíssimo (0.01–0.1) para progresso quase cru; o smoothing visual vem do Lenis + CSS |
| Lenis | Smooth scroll global e modo infinito em páginas de galeria | O site auditado usa Lenis inclusive com infinite: true para loops verticais de imagens |
Framer Motion (motion) | Transições de rota (template.tsx) e estados de UI pontuais | Substitui o papel do barba.js do site original dentro do paradigma App Router |
| OGL (ou WebGL puro) | Canvas de gradiente com blur que segue o mouse | O original usa WebGL puro de poucos KB; OGL dá o mesmo resultado com menos boilerplate que three.js |
| CSS custom properties e transitions | O verdadeiro motor de animação | transform: calc(...) consumindo --p1/--p2; transitions com delays por data-d; keyframes para marquee |
Não é necessário nenhum plugin de carrossel: as listas horizontais usam overflow + drag nativo. Se preferir abstração, Embla Carousel é compatível com todos os padrões daqui.
O mapa dos 18 motions
| # | Motion | Uso típico | Gatilho |
|---|---|---|---|
| 1 | Splash/preloader + entrada orquestrada | Primeira carga do site | Load |
| 2 | Transição de página (saída/entrada) | Toda navegação interna | Navegação |
| 3 | Sistema de texto clip/split | Títulos e parágrafos, o site inteiro | Load/Scroll |
| 4 | Motor scroll → CSS vars (--p1/--p2) | Base dos motions 5, 12, 13, 14, 17, 18 | Scroll |
| 5 | Hero collage com zoom progressivo | Hero da home | Scroll |
| 6 | Smooth scroll + scroll infinito | Global + galerias de detalhe | Scroll |
| 7 | Menu card 3D | Header (botão Menu) | Clique |
| 8 | Botões ghost + underlines animados | Botões e links globais | Hover |
| 9 | Cursor customizado com labels | Desktop, áreas de mídia | Mouse |
| 10 | Preview flutuante em hover de lista | Listas de itens | Hover |
| 11 | Arquivo: reveals, sort sticky, filtros | Páginas de listagem | Scroll/Clique |
| 12 | Painéis sobre background sticky | Seção de destaque na home | Scroll/Hover |
| 13 | Vídeo com expansão por clip-path | Seção de vídeo institucional | Scroll |
| 14 | Página de detalhe split-scroll | Detalhe de projeto/case | Scroll |
| 15 | Click-to-zoom de imagem (FLIP) | Galerias de detalhe | Clique |
| 16 | Lista horizontal com drag | Relacionados/próximos itens | Arraste |
| 17 | Sprites decorativos (pop + parallax) | Página institucional | Load/Scroll |
| 18 | Rodapé composto | Rodapé global | Scroll/Mouse |
Reveals genéricos de seção não têm número próprio: eles reutilizam o sistema de texto do Motion 3 e o padrão de fade/clip com data-shown via IntersectionObserver.
Passo 1 — A fundação: infraestrutura de animação
Tudo depende deste passo. Aqui nasce o provider de smooth scroll, o registro único dos plugins GSAP, os tokens de easing no CSS e os dois hooks que o site inteiro consome. Repare em três decisões que costumam ser ignoradas em tutoriais e que aqui são inegociáveis: o cleanup do Lenis em três passos (sem ele, cada navegação vaza um ticker), os easings morando no CSS (porque é o CSS que anima) e o hook de prefers-reduced-motion desde o dia zero — acessibilidade não é camada final, é fundação.
Em um projeto Next.js 14+ com App Router, TypeScript e Tailwind, configure a
infraestrutura de animação:
1. Instale: gsap @gsap/react lenis motion ogl
2. Crie um componente client "SmoothScrollProvider" que:
- Instancia Lenis com { lerp: 0.1, wheelMultiplier: 1, smoothWheel: true }
- Sincroniza com ScrollTrigger usando uma função nomeada (necessária
para o cleanup):
const update = (time: number) => lenis.raf(time * 1000);
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(update);
gsap.ticker.lagSmoothing(0);
- Cleanup COMPLETO no unmount (nesta ordem):
gsap.ticker.remove(update); // sem isso o ticker segue chamando
lenis.off('scroll', ScrollTrigger.update); // raf() de instância
lenis.destroy(); // morta (leak)
- Exponha a instância via React Context com um hook useLenis(), para
outros componentes chamarem lenis.stop()/start() (menu, zoom).
3. Registre os plugins uma única vez num arquivo lib/gsap.ts:
gsap.registerPlugin(ScrollTrigger, useGSAP) e exporte gsap.
Esse arquivo só deve ser importado por componentes client.
4. Crie o MOTOR CENTRAL do site: hook "useScrollProgress" (detalhado no
Motion 4) que lê elementos [data-scroll-trigger] e escreve --p1/--p2.
5. Crie um hook usePrefersReducedMotion() lendo a media query
(prefers-reduced-motion: reduce); todos os componentes de animação
devem pular ou reduzir animações quando true.
6. Defina no globals.css os tokens de easing e duração como CSS vars
(o site inteiro anima via CSS, então os easings moram no CSS):
:root {
/* aproximações CSS dos easings GSAP (valores de easings.net) */
--ease-power2-out: cubic-bezier(0.5, 1, 0.89, 1); /* quadOut */
--ease-power4-out: cubic-bezier(0.25, 1, 0.5, 1); /* quartOut */
--ease-power2-in-out: cubic-bezier(0.45, 0, 0.55, 1); /* quadInOut */
--a-clip-s: 1s; /* duração dos reveals de texto */
--a-global-t-s: 0.8s; /* duração de UI global (menu, header) */
--a-hover-s: 0.4s; /* duração de hovers */
--vh: 1vh; /* corrigido via JS p/ mobile (resize) */
}
7. Envolva {children} no layout.tsx raiz com <SmoothScrollProvider>.
Entregue os arquivos completos e o layout.tsx atualizado.Passo 2 — O motor: scroll vira CSS custom property (Motion 4)
Este é o coração de tudo, e é invisível por definição: é a fiação. Um único mecanismo alimenta o hero que cresce, os painéis que deslizam, o vídeo que expande e o rodapé que se revela. Implemente-o antes de qualquer motion visual — os motions 5, 12, 13, 14, 17 e 18 são só consumidores dele.
O funcionamento: uma factory de ScrollTrigger varre os elementos marcados com data-scroll-trigger, lê os atributos data-start e data-end e, com scrub curtíssimo, escreve em cada elemento duas variáveis — --p1 (progresso, 0 a 1) e --p2 (o inverso). Todo o resto é CSS: transform: translateY(calc(var(--p1) * -50vh)), scale(calc(1 + var(--p2) * 0.5)). Compositor-friendly, zero re-render de React, e os motions viram declarativos no markup.
O detalhe que separa o funcional do profissional: inicializar --p1: 0 e --p2: 1 no CSS, não só via JS. É isso que faz o HTML do servidor já renderizar o estado de topo, sem flash de layout no primeiro paint — essencial num projeto Next.js com SSR.
Crie o motor de progresso de scroll em Next.js:
1. Hook client "useScrollProgress(scopeRef)" que, dentro de useGSAP:
gsap.utils.toArray('[data-scroll-trigger]', scope).forEach((el) => {
ScrollTrigger.create({
trigger: el,
start: el.dataset.start ?? 'top bottom',
end: el.dataset.end ?? 'bottom top',
scrub: 0.01,
onUpdate: (self) => {
el.style.setProperty('--p1', String(self.progress));
el.style.setProperty('--p2', String(1 - self.progress));
}
});
});
Inicialize --p1: 0 e --p2: 1 via CSS (:where([data-scroll-trigger])
{ --p1: 0; --p2: 1 }) para o SSR renderizar o estado de topo sem
flash de layout.
2. Sintaxe declarativa no markup (idêntica ao site auditado):
<section data-scroll-trigger data-start="top top"
data-end="bottom+=20% top">...</section>
3. Os elementos filhos consomem as vars SÓ com transform/opacity
(nunca width/height/top: causam layout). Exemplos:
.bg { transform: scale(calc(1 + var(--p2) * 0.5)); }
.card { transform: translateY(calc(var(--p1) * -100vh)); }
.box { border-radius: calc(var(--r) * var(--p2)); }
4. will-change: transform apenas nos elementos que de fato animam, e
de preferência togglado (via classe) quando a seção está no viewport.
5. Chame ScrollTrigger.refresh() após load de imagens/fontes e após
transições de rota (Motion 2).
6. prefers-reduced-motion: não crie os triggers; congele --p1: 0/--p2: 1
(estado inicial estático).Passo 3 — A assinatura tipográfica: texto clip/split (Motion 3)
Nenhum texto do site simplesmente aparece. Títulos entram letra por letra, cada caractere subindo de trás de uma linha de corte invisível, com atraso mínimo entre eles — o efeito onda. Textos menores entram por palavra ou por linha. E o mesmo mecanismo, ao contrário, faz os textos saírem nas transições de página. É a assinatura "editorial de luxo" do site inteiro, e é usada por praticamente todos os outros motions — por isso vem antes deles.
A mecânica: cada unidade de texto é envolvida num <span class="o"> (máscara com overflow: hidden) contendo um <span class="t"> (o texto, começando em translateY(110%)). A animação é uma transition CSS disparada pela mudança do atributo data-shown, que um IntersectionObserver (ou o load do Passo 4) alterna. Os delays em cascata vêm do índice do caractere, via CSS var.
Crie o sistema de reveal de texto em Next.js:
1. Componente client "SplitReveal" com props:
type: 'chars' | 'words' | 'lines', delay (s entre unidades,
default 0.05), trigger: 'load' | 'inview', as: tag do elemento.
2. Split: use SplitText do GSAP (gratuito no 3.13+) com
{ type, mask: type } dentro de useGSAP, apenas para GERAR os
wrappers (.o overflow-hidden > .t); a animação em si é CSS.
Aguarde as fontes com autoSplit: true (ou document.fonts.ready)
para não quebrar linhas erradas.
3. CSS:
.split .t {
display: inline-block;
transform: translate3d(0, 110%, 0);
transition: transform var(--a-clip-s) var(--ease-power4-out),
opacity 0.6s;
transition-delay: var(--d, 0s);
}
[data-shown="true"] .t { transform: translate3d(0, 0, 0); }
Atribua --d por unidade: style={{ '--d': `${i * delay}s` }}.
4. trigger 'inview': hook useInView (IntersectionObserver,
threshold 0.2, once) seta data-shown="true".
trigger 'load': reage à classe is-loaded-a do Motion 1.
5. Compatibilidade com o Motion 2: quando <html> tem .is-leaving, o CSS
global sobrescreve com saída (translateY(-50%), fade, delay 0).
6. Cuidados: aria-label com o texto original no elemento pai e
aria-hidden nos spans (leitores de tela não devem soletrar);
prefers-reduced-motion troca tudo por fade simples.
7. Kerning fino: exponha uma prop para ajustes por caractere
(letter-spacing negativo em pares específicos), aplicada via
data-char, como faz o site auditado.Passo 4 — Estados globais: primeira carga e transições de página
Estes dois motions estabelecem as classes de estado (is-loaded, is-leaving) que o CSS inteiro referencia. Sem eles, os reveals do Passo 3 não têm quem os dispare no load, e a navegação vira corte seco.
Motion 1 — Splash e entrada orquestrada
A página não estoura na tela de uma vez. Primeiro tudo fica invisível por um instante; então os elementos do topo entram em coreografia: as letras do logo sobem uma a uma de dentro de uma máscara, os itens do menu aparecem em sequência, e só então o conteúdo principal começa o próprio reveal. Cortina de teatro abrindo, tudo no seu tempo.
E aqui está a elegância da arquitetura: nenhum tween JS. O <html> começa com uma classe de "não carregado"; quando fontes e assets críticos carregam, o JS adiciona is-loaded (e, um frame depois, is-loaded-a). Todo o resto são transitions CSS com transition-delay por elemento — um atributo data-d="1..N" que vira transition-delay: calc(var(--d) * 0.05s).
Implemente a entrada orquestrada em Next.js:
1. No globals.css: body { visibility: hidden } e
.is-loaded body { visibility: visible } (anti-FOUC bruto, igual ao
padrão de sites premium). Inclua um <noscript> com CSS alternativo
que força visibility: visible (acessibilidade sem JS).
2. Componente client "LoadOrchestrator" montado no layout raiz:
- Aguarda document.fonts.ready e o load dos assets críticos do hero
(Promise.all com decode() das imagens above-the-fold).
- Adiciona 'is-loaded' no <html>; num requestAnimationFrame seguinte,
adiciona 'is-loaded-a' (dois estágios permitem que o CSS distinga
"pronto" de "pode animar").
3. Elementos do header (letras do logo, itens de nav) usam o padrão de
máscara do Motion 3, cada um com atributo data-d="1..N". No CSS:
[data-d] .t { transition-delay: calc(var(--d) * 0.06s) }
(passe o número para a CSS var --d via style inline; não use attr()
em calc, o suporte dos browsers ainda é limitado).
Estado inicial: translateY(110%); com .is-loaded-a: translateY(0).
4. Opcional: logotipo do splash central com fade 1.2s antes do header.
5. prefers-reduced-motion: pule os delays e revele tudo com fade curto.
6. Garanta que a orquestração rode UMA vez por sessão (sessionStorage);
em navegações internas, quem manda é o Motion 2.Motion 2 — Transição de página
Ao clicar num link, a página atual não some num piscar: os textos escorregam para cima e desmancham em fade (~0.6s), e a página nova entra com os títulos subindo das máscaras, na mesma linguagem do primeiro load. A navegação vira continuação da coreografia.
No site original esse papel era do barba.js. Em Next.js, o App Router assume: Link e prefetch nativos cuidam da troca; a saída vem de uma classe global is-leaving aplicada antes do router.push (com um pequeno await), e a entrada vem do template.tsx remontando e o sistema de reveal reagindo ao novo mount.
Implemente transições de página em Next.js App Router:
1. Crie um TransitionProvider (client) com contexto { isLeaving,
navigate(href) }:
- navigate: seta isLeaving=true (adiciona classe 'is-leaving' no
<html>), aguarda ~600ms (duração da saída) e chama router.push(href).
- Todos os links internos usam um componente <TransitionLink> que
intercepta o clique e chama navigate (mantendo <Link> por baixo
para preservar prefetch).
2. CSS de saída (mesma linguagem do site auditado):
.is-leaving .c-clip .t, .is-leaving .c-fade {
transition: all 0.6s var(--ease-power2-in-out) !important;
transition-delay: 0s !important; /* ignora delays de entrada */
opacity: 0 !important;
transform: translateY(-50%) !important;
}
3. Entrada: template.tsx remonta a cada navegação; nele, um efeito
remove 'is-leaving' e re-dispara o pipeline do Motion 3 (os títulos
da nova página entram pelas máscaras). Um fade global do container
(Framer Motion, opacity 0→1, 0.35s) cobre o instante da troca.
4. Ao trocar de rota: lenis.scrollTo(0, { immediate: true }) antes da
entrada, e ScrollTrigger.refresh() depois que as imagens da nova
página carregarem.
5. Acessibilidade: anuncie a navegação (aria-live) e respeite
prefers-reduced-motion com corte simples de fade.
6. Não bloqueie navegações do browser (voltar/avançar): detecte popstate
e pule a fase de saída nesses casos.Passo 5 — O chrome: menu card 3D, botões e underlines
O menu valida os estados globais que você acabou de criar, e os botões são a vitória rápida que aparece em todo lugar do site.
Motion 7 — Menu card 3D com flip de label
Ao clicar em "Menu", um cartão arredondado se materializa no canto superior: ele assenta com sensação de profundidade (escala + deslocamento sob perspectiva 3D, como uma carta pousando na mesa), enquanto o rótulo do botão vira de "Menu" para "Close" com um flip vertical. Dentro, os links entram em cascata, com contadores discretos ao lado.
O truque de arquitetura: sem AnimatePresence. O cartão existe sempre no DOM (ótimo para o SEO dos links) e alterna estado por classe is-menu-opened no <html>. O pai tem perspective: 200vw; o cartão sai de scale(0.95) translateY(1rem) + opacity 0 para identidade, tudo transition CSS.
Crie um componente client "MenuCard" em Next.js:
1. Estrutura fixa no canto: div.menu (pointer-events none fechado) >
div.in (perspective: 200vw) > [header com botão toggle] +
div.menu-bg (fundo arredondado) + div.menu-body (conteúdo).
2. Estados via classe no <html> (useEffect togglando 'is-menu-opened'):
.menu-bg, .menu-body {
transform: scale(.95) translateY(1rem);
transform-origin: top center; opacity: 0;
transition: opacity .3s,
transform var(--a-global-t-s) var(--ease-power4-out);
}
.is-menu-opened .menu-bg, .is-menu-opened .menu-body {
transform: scale(1) translate(0); opacity: 1;
}
O site auditado define perspective mas mantém rotação em 0 (a
profundidade vem do scale + translate sob perspective). Se quiser
acentuar o 3D, adicione rotateX(-4deg) ao estado fechado (opcional).
3. Botão toggle com flip de label: máscara overflow-hidden com DUAS
divs empilhadas ("Menu" / "Close"); aberto → translateY(-100%)
revela "Close". O mesmo botão dentro do cartão fecha (aria-expanded,
aria-controls corretos).
4. Links principais com o split do Motion 3 (delays em cascata) e
contador discreto ao lado (número de itens, vindo do CMS);
links secundários/sociais em grade; seletor de idioma em bolinhas
circulares (aria-current no ativo).
5. Comportamento: Escape fecha; clique fora fecha; lenis.stop()/start()
ao abrir/fechar; foco preso no cartão enquanto aberto e devolvido
ao botão ao fechar; data-lenis-prevent se o cartão rolar.
6. Ao navegar por um link do menu: feche o cartão em paralelo à
transição do Motion 2.
7. prefers-reduced-motion: fade simples sem rotate/scale.Motion 8 — Botões ghost e underlines animados
Duas famílias de micro-interação, 100% CSS. Nos botões-pílula, o rótulo escorrega para cima e é substituído por uma cópia idêntica vinda de baixo (flip com máscara), enquanto um círculo de cor infla de dentro do botão até preencher a pílula — o fundo nasce redondo e se espalha. Nos links de texto, um sublinhado desliza para fora e volta pelo outro lado, ou se desenha da esquerda para a direita.
O micro-detalhe que dá o acabamento: no flip, a primeira cópia viaja para -120% e a segunda para -100% — esse descompasso mínimo entre as duas é o micro-stagger que faz o movimento parecer orgânico em vez de mecânico.
Crie os componentes "GhostButton" e "UnderlineLink" em Next.js:
1. GhostButton (pílula com borda):
- Markup: <a class="btn"> <span class="o"> <span class="w">Label
</span><span class="w" aria-hidden>Label</span> </span> </a>
- CSS:
.btn { position: relative; overflow: hidden; border-radius: 2em;
border: 1px solid var(--col-border); }
.btn::after { content: ''; position: absolute; inset: 0;
background: var(--col-black); border-radius: 50%;
transform: translateY(100%); z-index: 0;
transition: border-radius .3s var(--ease-power4-out),
transform var(--a-global-t-s) var(--ease-power4-out); }
.btn .o { overflow: hidden; position: relative; z-index: 1; }
.btn .w { display: block; transition: transform
var(--a-global-t-s) var(--ease-power4-out),
color .3s; }
.btn .w:last-child { position: absolute; top: 100%; }
.btn:hover::after { border-radius: 2em; transform: translate(0); }
.btn:hover .w { transform: translateY(-100%); color: var(--col-white); }
.btn:hover .w:first-child { transform: translateY(-120%); }
(o -120% vs -100% cria o micro-stagger entre as cópias)
- Estado "current"/ativo reaproveita o hover de forma persistente.
2. UnderlineLink:
- Variante slide: <span class="b"> absoluta na base, translateX(-101%),
hover → translateX(0), transition var(--a-hover-s)
var(--ease-power2-out).
- Variante draw (links de texto corrido): background-image
linear-gradient, background-size 0 1px → 100% 1px via keyframes
no hover (permite "redesenhar" a cada hover).
3. Rotas internas: use next/link (ou o TransitionLink do Motion 2);
externos: <a> com rel="noopener".
4. Hover states só em dispositivos com hover real:
@media (hover: hover); em touch, os estados ficam estáticos.
5. focus-visible:ring sempre presente; nunca remova outline sem repor.Passo 6 — O cartão de visitas: hero collage com zoom progressivo (Motion 5)
O topo do site é uma colagem: uma imagem central e várias menores espalhadas ao redor, todas com cantos arredondados, entrando em cascata no load. Ao rolar, a mágica: a imagem central cresce continuamente até tomar a tela inteira, os cantos vão relaxando até o retângulo puro, e as satélites dão zoom interno e saem de cena. A rolagem parece entrar dentro da imagem.
Este é o primeiro grande consumidor do motor do Passo 2 — e o teste de que sua fundação está certa. A estrutura: container de 200–230vh de altura com um filho position: sticky; top: 0; height: 100vh. O container é um data-scroll-trigger de top top a bottom top; tudo dentro anima por CSS consumindo --p1 e --p2.
Crie um componente client "HeroCollage" em Next.js:
1. Estrutura:
<section class="hero" data-scroll-trigger data-start="top top"
data-end="bottom+=20% top"> <!-- height: 230vh -->
<div class="hero-sticky"> <!-- sticky top-0 h-screen,
overflow-hidden -->
<div class="hero-mv">…visual central…</div>
<div class="hero-img" data-x="0" data-y="0">…</div>
<!-- N imagens satélites posicionadas por data-x/data-y -->
</div>
</section>
2. CSS (consumindo o motor do Motion 4):
.hero-mv {
transform: scale(calc(0.6 + var(--p1) * 0.4 + var(--p2) * 0.1))
translate(calc(var(--hero-x) * var(--p2)),
calc(var(--hero-y) * var(--p2)));
border-radius: calc(var(--r) * var(--p2));
overflow: hidden;
}
.hero-img .img { transform: scale(calc(1 + var(--p1) * 0.3)); }
Posições das satélites via um mapa data-x/data-y → top/left/size
em CSS (grid virtual), como no site auditado.
3. Entrada no load: cada imagem tem data-delay (0, 0.1, 0.2, ...);
com .is-loaded-a, transitions de opacity + scale revelam a colagem
em cascata (padrão do Motion 1). Título do hero com o split de
chars do Motion 3, 2 linhas.
4. Imagens: next/image com fill + sizes; carregue versões @pre
(placeholder blur) e @2x. As satélites saem do DOM visual no fim
(opacity via --p1) mas mantêm o espaço.
5. Mobile: reduza a colagem (menos satélites, via CSS/data-mb) e
diminua a distância de scroll (200vh); gsap.matchMedia se precisar
de triggers diferentes.
6. prefers-reduced-motion: hero estático em 100vh com o visual central
já em escala final.Passo 7 — Seções em série: arquivo, painéis sticky e vídeo-íris
Com a base pronta, estes três motions saem em produção em série — todos reutilizam padrões que você já tem.
Motion 11 — Arquivo: reveals com border draw, sort sticky e filtros
Três comportamentos fazem uma listagem parecer viva. Ao rolar, cada linha divisória se desenha e o card sobe com fade em cascata, com zoom interno nas thumbs ao hover. O cabeçalho de ordenação gruda no topo e, ao colar, encolhe suavemente. E clicar em "Filter" abre um painel que desliza expandindo a altura, com as opções entrando em sequência.
Duas técnicas valem o destaque didático aqui. O sticky que encolhe usa um sentinel de 1px observado por IntersectionObserver — nada de listener de scroll medindo offsets. E o painel que expande usa grid-template-rows: 0fr → 1fr — o jeito moderno de animar altura sem medir um único pixel via JS.
Crie um componente client "ArchiveGrid" em Next.js:
1. Grid responsivo (1 → 3/4 colunas) de cards: thumb (next/image em
moldura overflow-hidden rounded), título, tags-pílula (GhostButton
do Motion 8 em versão mini).
2. Reveals: cada card com data-shown via useInView (once). Linha
divisória: .border { overflow: hidden; height: 1px }
.border > div { height: 100%; background: currentColor;
transform: translateY(-110%); transition: transform .8s
var(--ease-power2-out); } [data-shown] .border > div
{ transform: translateY(0); } Título/texto com o Motion 3.
3. Hover: thumb com img scale(1.05), transition .7s ease-out
(group-hover); cursor "View" (data-cursor do Motion 9).
4. Header de ordenação sticky:
- position: sticky; top: 0; background igual ao da página; z-index
acima do grid.
- Sentinel de 1px acima do header observado por IntersectionObserver;
quando sai do viewport → classe 'is-stuck' no header →
.title { transform: scale(0.8); transform-origin: left bottom;
transition: transform var(--a-global-t-s) var(--ease-power4-out) }
e paddings reduzidos (via transform/margins, não height).
5. Painel de filtros (slide toggle):
- Wrapper display: grid; grid-template-rows: 0fr;
transition: grid-template-rows .6s var(--ease-power4-out);
aberto → 1fr. Filho com min-height: 0; overflow: hidden.
- Seta do botão rotaciona 90°; opções entram com stagger (Motion 3).
- aria-expanded/aria-controls no botão.
6. Aplicar filtro: atualize a lista (searchParams/estado) e re-dispare
os reveals apenas dos cards novos; ScrollTrigger.refresh() após o
reflow.Motion 12 — Painéis sobre background sticky com swap por hover
Uma imagem grande fica fixa ao fundo enquanto cartões brancos deslizam por cima dela conforme você rola — a imagem parece parada no tempo atrás dos painéis, que passam como páginas. E dentro do painel de categorias, pairar sobre cada nome troca a imagem de fundo em fade.
A lição deste motion: o efeito básico não usa nenhum JS. O fundo é position: sticky; top: 0; height: 100vh num wrapper alto; os painéis são blocos normais com border-radius e z-index maior que rolam por cima. Stacking + sticky, só isso. O swap por hover reutiliza o mecanismo .now/.prev que você verá no Motion 10.
Crie um componente client "StickyShowcase" em Next.js:
1. Estrutura:
<section class="showcase"> <!-- altura: conteúdo -->
<div class="showcase-bg"> <!-- sticky top-0 h-screen,
z-index: 1 -->
<div class="bg-layer now" data-target="slug-1">…img…</div>
<div class="bg-layer" data-target="slug-2">…img…</div>
<!-- 1 camada por categoria, todas pré-montadas -->
</div>
<div class="showcase-panels"> <!-- z-index: 2, margin-top:
-100vh para sobrepor -->
<div class="panel">…título + destaque…</div>
<div class="panel">…lista de categorias…</div>
</div>
</section>
2. Painéis: background branco, border-radius no topo
(var(--r) var(--r) 0 0), height ~85-100vh cada, conteúdo
distribuído (flex column space-between). O efeito de "passar por
cima" é só stacking + sticky, sem JS.
3. Swap por hover: itens da lista com data-target; mouseenter troca
.now/.prev nas bg-layers (mesmo CSS de crossfade do Motion 10).
Sem hover ativo, mantenha a última categoria ou a default.
4. Reveals internos dos painéis com o Motion 3 (data-shown/useInView).
5. Ao clicar numa categoria: navegue com o TransitionLink (Motion 2).
6. Mobile: hover não existe; as camadas trocam por scroll (qual item
da lista está no centro) ou fixam a default. Valide o custo das
camadas montadas (limite N; lazy para além da dobra).Motion 13 — Vídeo com expansão por clip-path
O vídeo não começa em tela cheia: surge como uma janela recortada com cantos arredondados dentro da seção. Conforme você rola, o recorte se abre continuamente até o vídeo tomar a viewport inteira — uma íris de câmera acoplada à rolagem. É o motor do Passo 2 aplicado a uma única propriedade: clip-path: inset(... round ...) consumindo --p1.
Crie um componente client "ClipVideoSection" em Next.js:
1. Estrutura: section[data-scroll-trigger data-start="top bottom"
data-end="top top"] > div.video-full (height: 100vh) > video +
overlay opcional.
2. CSS da íris (consumindo --p1 do Motion 4; --p1 vai de 0 quando a
seção entra a 1 quando cola no topo):
.video-full {
will-change: clip-path;
clip-path: inset(
calc(var(--p2) * 20%) calc(var(--p2) * 15%)
calc(var(--p2) * 20%) calc(var(--p2) * 15%)
round calc(var(--r) * var(--p2) * 4)
);
}
(em --p1: 1 o inset zera e o raio zera: fullscreen puro)
Toggle will-change apenas enquanto a seção está no viewport.
Fechamento na saída (opcional): segundo elemento data-scroll-trigger
interno com data-start="bottom bottom" data-end="bottom top" e um
segundo par de vars aplicando o inset inverso.
3. Vídeo: autoPlay muted loop playsInline preload="metadata" poster;
sources webm antes de mp4; muted reforçado via ref em useEffect
(issue conhecida do React) + .play().catch(() => {}).
Versão mobile mais leve via <source media> ou swap por matchMedia.
4. Pausa fora do viewport (IntersectionObserver → video.pause()/play())
para economizar bateria/CPU.
5. prefers-reduced-motion: sem íris (clip-path none, aspect-ratio 16/9
estático) e vídeo pausado exibindo o poster.Passo 8 — Polish de desktop: cursor contextual e preview flutuante
Camada de acabamento, desktop apenas. Nos dois casos, a regra é a mesma: renderize somente em (hover: hover) and (pointer: fine) — em touch, os componentes retornam null e nada quebra.
Motion 9 — Cursor customizado com labels contextuais
Além do cursor do sistema, um pequeno ponto acompanha o mouse com atraso elástico. Sobre uma imagem clicável, ganha o rótulo "View"; sobre uma ampliável, "Zoom"; um halo desfocado acompanha por trás. O cursor vira um guia de affordance — ele conta o que cada área faz antes do clique.
A forma idiomática do movimento é gsap.quickTo em x/y (a única exceção justificada ao "CSS anima": seguir o mouse exige JS mesmo). E uma regra de acessibilidade que o site auditado respeita: nunca cursor: none. O ponto é um companheiro do cursor nativo, não um substituto.
Crie um componente client "CustomCursor" em Next.js:
1. Estrutura fixa (root do layout): div.cursor (fixed, top-0 left-0,
pointer-events-none, z-max) > div.cursor-body (ponto) +
div.cursor-label[data-type=view] + div.cursor-label[data-type=zoom]
+ div.cursor-blur (halo com filter: blur).
2. Movimento: dentro de useGSAP,
const xTo = gsap.quickTo(el, 'x', { duration: 0.4,
ease: 'power3.out' });
const yTo = gsap.quickTo(el, 'y', { duration: 0.4,
ease: 'power3.out' });
window mousemove → xTo(e.clientX); yTo(e.clientY).
O halo blur usa duration maior (0.8) para "arrastar" atrás.
3. Contexto: listener delegado de mouseover/mouseout no document;
target.closest('[data-cursor="view"|"zoom"]') define o rótulo
ativo (classe no cursor); os rótulos entram/saem pelo padrão de
máscara do Motion 3.
4. Marque as áreas no site: thumbs de listagem → data-cursor="view";
imagens ampliáveis (Motion 15) → data-cursor="zoom".
5. Renderize o componente apenas em (hover: hover) and (pointer: fine)
(matchMedia num useEffect; retorne null em touch). NUNCA esconda o
cursor nativo (cursor: none prejudica acessibilidade); o ponto é um
companheiro, não um substituto.
6. prefers-reduced-motion: siga o mouse sem lerp (posição direta) ou
desative o componente.Motion 10 — Preview flutuante em hover de lista
Uma lista aparentemente simples de nomes. Ao passar o mouse sobre um, uma miniatura brota flutuando ao lado do cursor, com leve zoom-out de assentamento. Deslize para o próximo nome e a imagem se troca em crossfade. Percorrer a lista vira folhear um catálogo invisível.
O segredo do crossfade contínuo é o par de classes .now/.prev: a imagem anterior fica um instante visível por baixo enquanto a nova assenta por cima. E todas as miniaturas são pré-renderizadas e empilhadas desde o início — o hover nunca espera rede.
Crie um componente client "HoverPreview" em Next.js:
1. Estrutura: div.popup (fixed top-0 left-0, pointer-events-none) >
div.popup-frame (tamanho fixo, ex. 200x200, rounded, overflow
hidden, opacity 0) > N divs.preview[data-target=slug], cada uma
absolute inset-0 com next/image (todas montadas desde o início:
o hover nunca espera rede).
2. CSS de troca (crossfade com assentamento):
.preview { opacity: 0; transform: scale(1.05);
transition: opacity .3s, transform 0s; }
.preview.now { opacity: 1; transform: scale(1); z-index: 2;
transition: opacity .3s, transform 1.2s; }
.preview.prev { opacity: 0; transform: scale(1); z-index: 1;
transition: opacity 0s .3s, transform 0s 1.2s; }
(a .prev segura a imagem anterior visível por baixo durante o fade
da nova; é isso que faz a troca parecer contínua)
3. Lógica: mouseenter num <li data-target=slug> → a preview
correspondente vira .now, a antiga vira .prev; mouseenter na lista →
frame opacity 1; mouseleave da lista → opacity 0 (após transição,
limpe .now/.prev).
4. Movimento: siga o mouse com gsap.quickTo (offset de ~20px do
cursor, duration 0.5, power3.out).
5. Desktop apenas ((hover: hover)); em touch a lista funciona sem
preview. Os itens seguem linkáveis e acessíveis por teclado
(preview é decorativa, aria-hidden).Passo 9 — A página de detalhe: split-scroll, zoom FLIP e relacionados
O conjunto mais interdependente do site — implemente os três juntos.
Motion 14 — Split-scroll com aside fixa
A página de projeto se divide em duas: à direita, um painel com título, ficha técnica e descrição que fica parado; à esquerda, a coluna de fotos desliza verticalmente conforme você rola, com a foto ativa destacada e um indicador circular de progresso no canto. A sensação de folhear o portfólio com uma mão enquanto a ficha permanece à vista.
De novo o motor do Passo 2: o wrapper alto define o comprimento do scroll, a coluna de mídia é position: fixed movida por transform: translateY(calc(-50% * var(--p1))), e até o indicador circular é a mesma var — stroke-dashoffset calculado de --p2 num SVG.
Crie um componente client "ProjectDetail" em Next.js:
1. Estrutura:
<div class="detail" data-scroll-trigger data-start="top top"
data-end="bottom bottom"> <!-- altura: N * espaço/foto -->
<div class="media-col"> <!-- fixed left, w-1/2 -->
<ul>…N figuras (imagens/vídeos)…</ul>
</div>
<aside class="info">…título, ficha (dl), descrição…</aside>
<!-- fixed right, w-1/2,
h-screen -->
<div class="progress">…indicador circular…</div>
</div>
2. CSS: .media-col { position: fixed; top: 0; height: 100%;
transform: translateY(calc(-50% * var(--p1))); }
(a lista tem o dobro da altura da viewport de percurso; --p1 cobre
o trajeto todo). Indicador circular: stroke-dashoffset =
calc(circunferência * var(--p2)) num SVG.
3. Item ativo: derive de --p1 (onUpdate do trigger já roda; num
callback adicional, calcule o índice e aplique classe ._on ao item:
fundo do placeholder some, cursor "Zoom" habilita).
4. A aside é fixed com pointer-events: none no wrapper e auto nos
interativos (títulos, links, botão copiar link), para o scroll
"atravessar" o painel.
5. Botão "copiar link" com flip de label Copy → Copied (padrão do
Motion 8; estado _ok temporário via clipboard API).
6. Mobile: desative o layout split (media-col relativa, aside após o
conteúdo, progresso oculto), via CSS/matchMedia; é o mesmo fallback
do site auditado.
7. Integre com o Motion 15 (zoom) e finalize com o Motion 16
(relacionados horizontais).Motion 15 — Click-to-zoom de imagem (FLIP)
Clique numa foto e ela cresce suavemente do lugar onde está até o centro da tela, enquanto o resto da página escurece e o scroll trava. Clicar de novo faz a foto voltar encolhendo exatamente para o buraco de onde saiu. Nada de modal que pisca: é a própria imagem viajando.
O nome da técnica é FLIP (First-Last-Invert-Play): medir o retângulo atual, calcular o transform até o retângulo final e animar só transform. O plugin Flip do GSAP (gratuito) faz isso nativamente.
Implemente o click-to-zoom em Next.js com GSAP Flip:
1. Registre o plugin Flip em lib/gsap.ts. Imagens ampliáveis envoltas
em <figure class="zoomable" data-cursor="zoom">.
2. Ao clicar:
- const state = Flip.getState(figure);
- Aplique a classe 'is-zoomed' (CSS posiciona a figura fixed,
centralizada, max 90vw/90vh, object-fit contain, z-index alto);
- Flip.from(state, { duration: 0.8, ease: 'power4.inOut',
absolute: true });
- Marque <html> com 'is-zoom-in': CSS aplica opacity .3 e
pointer-events: none no restante; um backdrop fixo captura o
clique de fechar; lenis.stop().
3. Fechar (clique/Escape): Flip de volta ao estado original
(getState do zoomed → remove classes → Flip.from), depois
lenis.start() e foco devolvido à figura.
4. Carregue a versão @2x da imagem ao ampliar (troque o src num
onStart; mantenha a @1x até a nova decodificar para não piscar).
5. Acessibilidade: role="button" + aria-expanded na figura, Escape
fecha, foco gerenciado; prefers-reduced-motion troca o FLIP por
fade rápido.Motion 16 — Lista horizontal com drag
No fim da página de detalhe, uma fileira de cartões corre horizontalmente: arraste com o mouse ou o dedo e os cartões deslizam. É a prateleira final que emenda uma navegação na outra.
A opção fiel ao site auditado dispensa bibliotecas: contêiner com overflow-x: auto, scrollbar oculta, drag-to-scroll com Pointer Events convertendo deltaX em scrollLeft, e data-lenis-prevent para o Lenis global não capturar o gesto.
Crie um componente client "RelatedRow" em Next.js:
1. Estrutura: div.row (overflow-x auto, scrollbar oculta via
scrollbar-width: none e ::-webkit-scrollbar { display: none },
data-lenis-prevent) > ul flex com N cards (flex 0 0 auto,
width ~40vw desktop / 80vw mobile, gap).
2. Drag-to-scroll com Pointer Events:
pointerdown → guarde startX/scrollLeft e setPointerCapture;
pointermove → row.scrollLeft = scrollLeft - (x - startX);
pointerup → release. Durante o drag, classe 'is-dragging'
(cursor grabbing) e suprima o clique se houve movimento > 8px
(preventDefault no click subsequente).
3. Inércia: o scroll nativo já dá momentum em touch; para mouse,
aplique um decay simples no pointerup (velocity * fricção via
requestAnimationFrame) ou aceite a parada seca (aceitável).
4. Cards: thumb com hover zoom (Motion 11), título com Motion 3,
cursor "View" (Motion 9). Primeiro/último item com padding lateral
var(--go) para respiro.
5. Acessibilidade: a lista é navegável por Tab normalmente (scroll
nativo acompanha o foco); setas de teclado opcionais.
6. Se o design pedir snap/paginação/autoplay: troque por Embla
Carousel mantendo o restante igual.Passo 10 — Scroll infinito nas galerias (Motion 6)
Depois da página de detalhe, ela ganha um superpoder raro: nas galerias de um item, a rolagem nunca acaba. Chegou na última imagem? A primeira reaparece emendada, em loop infinito, como um rolo contínuo de tecido.
A técnica: uma segunda instância de Lenis com infinite: true, montada num wrapper próprio, com o conteúdo duplicado para o loop fechar. E um cuidado que não é opcional: scroll infinito é hostil a teclado e leitores de tela — o fallback de lista estática precisa existir.
Implemente o scroll infinito de galeria em Next.js:
1. Componente client "InfiniteGallery" usado nas rotas de detalhe:
- Estrutura: div.wrapper (viewport, overflow hidden) > div.content
contendo a lista de imagens DUPLICADA (duas cópias idênticas,
para o loop emendar).
- const lenis = new Lenis({ lerp: 0.1, wrapper: wrapperRef.current,
content: contentRef.current, infinite: true });
- rAF próprio (gsap.ticker.add com função nomeada) e cleanup em
3 passos como no Prompt 0. PAUSE o Lenis global da página
(useLenis().stop()) enquanto esta instância domina o scroll,
e restaure no unmount.
2. No callback lenis.on('scroll'), normalize o progresso
(scroll % alturaDeUmaCopia) e escreva --p1 no content para efeitos
dependentes de posição (ex.: item central destacado).
3. Barra/indicador de progresso circular fixo lendo o mesmo progresso.
4. Acessibilidade: infinite scroll confunde navegação por teclado;
ofereça lista estática alternativa (prefers-reduced-motion e
navegação por Tab desativam o modo infinito, caindo para scroll
normal com a lista única).
5. Mobile: valide o comportamento de touch; se necessário, desative
infinite e use scroll nativo (gsap.matchMedia).Passo 11 — O fechamento: sprites decorativos e o rodapé em três atos
Motion 17 — Sprites decorativos (pop + camadas parallax)
Na página institucional, elementos gráficos da marca brotam na tela — no caso da Plantica, literalmente flores: cada uma puxa do nada com um pop elástico (nasce em escala zero e assenta no tamanho final), em posições e tempos diferentes — um jardim florescendo em stop-motion ao redor do texto. Outros ficam em camadas de fundo que se movem em velocidades diferentes durante a rolagem, dando profundidade de diorama.
A sacada de performance: tudo vem de sprite sheets — uma imagem única com todos os recortes, cada elemento exibido via background-position com CSS vars. Dezenas de elementos decorativos, um único request.
Crie um componente client "SpriteDecor" em Next.js:
1. Gere a sprite sheet dos elementos (uma .webp com todos os recortes)
e um JSON de manifesto: [{ name, fx, fy, w, h, x, y, layer,
delay }] (posição no sheet + posição/camada na tela).
2. Componente:
<div class="sprite" data-type="pop|bg" data-scroll-trigger>
{frames.map(f =>
<div class="frame" style={{ '--fx': f.fx, '--fy': f.fy,
'--sw': SW, '--sh': SH, '--w': f.w, '--h': f.h,
left: f.x, top: f.y, '--d': `${f.delay}s` }}>
<div class="img" />
</div>)}
</div>
CSS: .frame .img { width: calc(var(--w) * 1px);
height: calc(var(--h) * 1px);
background-image: url(sprite.webp);
background-position: calc(var(--fx) * -1px) calc(var(--fy) * -1px);
background-size: calc(var(--sw) * 1px) calc(var(--sh) * 1px); }
(no site auditado as contas usam unidade vw para escalar com a tela;
parametrize a unidade)
3. Tipo pop: .img { transform: scale(0); transition: transform 1.8s
var(--d) var(--ease-power4-out) } → [data-shown] .img
{ transform: scale(1) } (dispare com useInView; o easing out
longo dá o assentamento "elástico").
4. Tipo bg: cada camada num wrapper com
transform: translateY(calc(var(--p1) * var(--depth) * -10vh))
(depth 0.3/0.6/1.0 por camada), consumindo o motor do Motion 4.
5. Todos os frames com pointer-events: none e aria-hidden (decoração).
6. prefers-reduced-motion: pops entram já em scale(1) com fade;
parallax congelado.Motion 18 — Rodapé composto: parallax reveal, marquee e canvas de blur
Três atos no mesmo lugar. Primeiro, o rodapé não rola para dentro: ele é revelado — a página termina e o conteúdo desliza para cima como uma cortina, mostrando o rodapé que já estava embaixo, movendo-se em velocidade reduzida. Segundo, uma faixa-convite desliza continuamente, estilo letreiro. Terceiro, atrás da faixa, uma mancha de cor desfocada segue o mouse como luz atravessando vidro fosco — um canvas WebGL de poucos KB.
Repare que cada ato usa a ferramenta mínima necessária: o reveal é o motor de scroll de sempre; o marquee é CSS puro num Server Component, zero JS; só a mancha de luz justifica WebGL.
Crie o rodapé composto em Next.js (3 sub-componentes):
1. "FooterReveal": footer com
<div class="footer-body" data-scroll-trigger data-start="top bottom"
data-end="bottom center">…conteúdo…</div>
CSS: .footer-body { height: 100vh;
transform: translateY(calc(var(--p2) * -50vh)); }
(entra "atrasado" e alcança a posição natural quando o scroll
termina: efeito de cortina revelando). Corpo da página com z-index
acima do footer e background sólido.
2. "InviteMarquee" (Server Component, zero JS):
div.mq (overflow hidden, flex) > ul.mq-ul (flex, width: 200%) >
2 li.mq-li idênticos (cada um com a frase repetida K vezes);
CSS: .mq-li { will-change: transform;
animation: marquee 30s linear infinite; }
@keyframes marquee { from { transform: translateX(0) }
to { transform: translateX(-100%) } }
(com DOIS itens de 100% cada, o -100% emenda perfeito)
O marquee inteiro é um link para a página de contato; versão
sr-only do texto fora da faixa; animation: none em
prefers-reduced-motion; pausa opcional no hover.
3. "MouseBlurCanvas" (client): canvas absoluto atrás do marquee
(pointer-events: none), com OGL:
- Plane fullscreen + fragment shader: 2-3 gradientes radiais de
cores da marca somados, centro deslocado por u_mouse (uniform
vec2) suavizado com lerp no rAF, u_time para respiração lenta.
- Resize observer atualiza a resolução; renderer.dispose() e
cancelamento do rAF no cleanup.
- Fallback sem WebGL: gradiente CSS estático.
- Renderize só quando o footer está no viewport
(IntersectionObserver liga/desliga o rAF).
4. Complete com colunas de sitemap (links com Motion 8), logo grande
com reveal (Motion 3) e créditos.As 15 armadilhas que vão te morder (leia antes de codar)
Cada item desta lista custou horas de debug de alguém. Passe por ela antes de escrever a primeira linha — e volte quando algo "misteriosamente" quebrar.
"use client"obrigatório em todo componente que toca GSAP, Lenis ou observers.- Sempre
useGSAP({ scope })em vez deuseEffectcru: ocontext.revert()automático mata triggers órfãos ao navegar entre rotas. - Cleanup do Lenis é em 3 passos:
gsap.ticker.remove(update)+lenis.off('scroll', ...)+lenis.destroy(). Vale para a instância global e para as locais (galeria infinita). - Trava de scroll com Lenis: use
lenis.stop()/start()(menu, zoom), não apenasoverflow: hidden; edata-lenis-preventem áreas com scroll próprio. - CSS vars + SSR: inicialize
--p1: 0; --p2: 1no CSS, não só via JS — senão há flash no primeiro paint. - Só
transform,opacityeclip-pathnas animações por CSS var; nunca anime width, height, top ou left (layout thrashing a cada frame de scroll). will-changecirúrgico: só nos elementos que animam, de preferência togglado quando a seção está visível;will-changeglobal degrada memória de GPU.ScrollTrigger.refresh()após load de imagens e fontes e após cada transição de rota; posições calculadas antes do layout final ficam erradas.- Split de texto só depois das fontes (
autoSplit: trueoudocument.fonts.ready); e semprearia-labelno pai +aria-hiddennos spans. gsap.matchMedia()para variações responsivas de trigger;ScrollTrigger.matchMediaestá deprecado desde o GSAP 3.11.- Estados globais no
<html>(is-loaded,is-leaving,is-menu-opened,is-zoom-in): centralize num pequeno store que espelha as classes; nunca toggles espalhados sem dono. - Cursor custom e previews: desktop apenas (
(hover: hover) and (pointer: fine)); em touch, os componentes retornam null. E nuncacursor: none. - WebGL: dispose sempre (renderer, geometrias, texturas) e rAF cancelado no unmount; renderize apenas com o canvas no viewport.
- Infinite scroll é hostil a teclado e leitores de tela: ofereça sempre o fallback de lista estática.
prefers-reduced-motionem tudo: o padrão CSS-first facilita — um bloco único de media query pode neutralizar transitions e keyframes globalmente.
Por onde começar hoje
Se o guia inteiro parece muito, comece pelo trio que paga o investimento mais rápido: o Passo 1 (a fundação — sem ela nada funciona), o Passo 2 (o motor --p1/--p2 — meia hora de trabalho que destrava seis motions) e o Motion 8 (botões e underlines — CSS puro, resultado imediato, usado em todo lugar).
Com esses três no lugar, cada motion seguinte é incremental: você já tem os estados, o motor e a linguagem. Vale também abrir o plantica.net numa aba ao lado e rolar devagar, reconhecendo cada padrão deste guia em ação no original — não há exercício melhor para treinar o olho.
E a filosofia fica no músculo: o JS escreve variáveis e classes; o CSS faz o movimento. É ela — mais do que qualquer biblioteca — que separa os sites que parecem premium dos que só têm animação.
Leia também
Motions premium em Next.js: os 15 padrões do site do Studio Modular, passo a passo
A arquitetura componentizada do site do Studio Modular — módulos com gates, scroll nativo e CSS de ponta — em 15 padrões de motion com prompts prontos.
- motion
- gsap
Motions premium em Next.js: os 16 padrões do site da Beetogreen, passo a passo
Estados por seção, GSAP mínimo e CSS máximo: os 16 padrões de motion do site da Beetogreen em Next.js, com replay por chave e o prompt pronto de cada um.
- motion
- gsap
Motions premium em Next.js: os 18 padrões do site da ANAI, passo a passo
A arquitetura híbrida do site da ANAI — CSS vars, tweens por seção e canvas — destrinchada em 18 padrões de motion com prompts prontos.
- motion
- gsap


