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

Sumário
- A filosofia: três camadas, cada uma no seu lugar
- 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 duplo: --pg-value no scroll, --x/--y no mouse (Motion 4)
- Passo 3 — A tipografia que acende: scramble e sweep (Motion 5)
- Passo 4 — Estados globais: preloader, transições e header inteligente
- Passo 5 — Vitória rápida: underlines com origem trocada e setas que respiram (Motion 9)
- Passo 6 — O cartão de visitas: a janela que se abre e o lens flare
- Passo 7 — Produção em série: clips, parede em contra-parallax e rodapé
- Passo 8 — Identidade de UI: o menu com pontos vivos e o cursor em duas camadas
- Passo 9 — O primeiro slider: vitrine com título central que troca (Motion 12)
- Passo 10 — A joia da coroa: slider sincronizado com anéis em canvas (Motion 13)
- Passo 11 — Polish rápido: tilt 3D de imagens pelo mouse (Motion 8)
- Passo 12 — As camadas "wow": partículas WebGL e física DOM
- As 18 armadilhas que vão te morder (leia antes de codar)
- Por onde começar hoje
Este é o segundo estudo de engenharia reversa da série. No guia da Plantica, o segredo era radical: o JS quase não animava nada, só escrevia variáveis e classes para o CSS mover. O site desta vez responde à mesma pergunta — como um site premium de verdade organiza suas animações? — com uma arquitetura diferente, e o contraste entre as duas é a melhor aula de motion que você vai ter este ano.
O auditado da vez é a ANAI (anaiwood.com): a Anai Wood Factory, serraria japonesa fundada em 1964, que se apresenta com o lema "We Cultivate for Posterity" — e com um site à altura, construído em Nuxt/Vue com GSAP, SplitText, Three.js e Matter.js. Do preloader com contador aos anéis de crescimento de madeira desenhados em canvas, tudo ali tem intenção. Os 18 padrões abaixo foram catalogados auditando esse site e traduzidos para o ecossistema Next.js; nomes, textos e imagens são placeholders parametrizáveis. O que você leva é o padrão de movimento, a técnica e um prompt de implementação pronto para colar no seu agente de IA ou usar como especificação.
Como no guia anterior, a ordem dos passos não é a ordem do catálogo: é a ordem de construção, do alicerce ao açúcar.
A filosofia: três camadas, cada uma no seu lugar
A Plantica era "CSS-first, JS-orquestrado". A ANAI é uma arquitetura híbrida em três camadas — e entender a fronteira entre elas é entender o site inteiro:
- Camada 1 — variáveis contínuas. Um motor de scroll escreve o progresso de cada seção numa CSS custom property (
--pg-value), e um rastreador global escreve a posição do mouse em duas vars no<html>(--x/--y, com lerp). O CSS consome tudo comcalc(): clips, parallax, tilts e flares rodam no compositor, sem um frame de JS. - Camada 2 — tweens de entrada. Um observer de seções central dispara animações GSAP pontuais quando cada bloco entra no viewport: fades, varreduras de parágrafo e o scramble de cor dos títulos. São tweens de uma vez só — coreografia de chegada, não de scroll.
- Camada 3 — canvas isolado. Os momentos "wow" vivem em canvas próprios: anéis concêntricos em 2D sincronizados ao slider, partículas ambientais em WebGL e física de elementos com Matter.js. Todos opcionais, todos degradáveis, nenhum contamina as outras camadas.
Amarrando tudo, os estados globais viajam por classes no <html>: --is-loading, --is-transition, --is-scrolled, --is-menu-active. O CSS inteiro reage a elas.
A stack e o papel de cada peça
| Biblioteca | Papel no projeto | Por quê |
|---|---|---|
| GSAP 3.13+ e @gsap/react | Tweens de entrada por seção, scramble de texto (SplitText), timelines do preloader | GSAP é 100% gratuito, incluindo todos os plugins, desde a aquisição pela Webflow |
| ScrollTrigger (plugin GSAP) | Escrever --pg-value por seção (substitui o motor de scroll próprio do site auditado) | Mesmo resultado com menos código proprietário; scrub curto + Lenis reproduzem o feel |
| Lenis | Smooth scroll com lerp no desktop (touch fica nativo, mesma decisão do site auditado) | Entrega scroll, velocity e direction prontos, que alimentam o header inteligente |
| SplitText (plugin GSAP) | Split de títulos em chars para o reveal por cor com stagger aleatório | Assinatura tipográfica do site auditado |
| Three.js (ou OGL) | Partículas ambientais (Points + noise no vertex shader) | Camada decorativa "viva" sobre imagens de atmosfera |
| Matter.js | Física de micro-elementos DOM (corpos sincronizados + corpo-cursor) | Micro-interações lúdicas com custo baixo |
| CSS custom properties e transitions | Clips, tilts, parallax e underlines consumindo --pg-value/--x/--y | Animações de scroll e mouse rodam no compositor, sem re-render |
Nenhum plugin de carrossel é necessário: os sliders do site auditado são próprios (translate + estado). Se preferir abstração, Embla Carousel cobre os dois sliders deste guia.
O mapa dos 18 motions
| # | Motion | Uso típico | Gatilho |
|---|---|---|---|
| 1 | Preloader com progresso + handoff | Primeira carga | Load |
| 2 | Transição de rota com máscara | Toda navegação interna | Navegação |
| 3 | Scroll suave + header inteligente | Global | Scroll |
| 4 | Motor progresso → CSS vars + mouse global | Base dos motions 6, 7, 8, 14, 15, 18 | Scroll/Mouse |
| 5 | Texto: color scramble + sweep | Títulos e parágrafos | Inview |
| 6 | Hero: janela clip que abre | Hero da home | Load/Scroll |
| 7 | Lens flare em camadas pelo mouse | Hero/atmosferas | Mouse |
| 8 | Tilt 3D de imagens pelo mouse | Imagens editoriais | Mouse/Hover |
| 9 | Underlines e setas com micro-animação | Links e navegações | Hover |
| 10 | Menu fullscreen com botão "vivo" | Header global | Clique |
| 11 | Cursor customizado em 2 camadas | Desktop | Mouse |
| 12 | Slider de vitrine com título central | Vitrine na home | Hover/Clique |
| 13 | Slider sincronizado com anéis em canvas | Vitrine de projetos | Clique/Arraste |
| 14 | Colunas em contra-parallax | Seção temática imersiva | Scroll |
| 15 | Clip reveals de mídia por scroll | Subpáginas e galerias | Scroll |
| 16 | Partículas ambientais WebGL | Atmosferas/hero | Automático |
| 17 | Física de elementos DOM | Micro-interações lúdicas | Mouse |
| 18 | Rodapé com assentamento parallax | Rodapé global | Scroll |
Reveals genéricos de seção (fades com autoAlpha, sweeps, scrambles) são disparados pelo observer de seções do Motion 5 e não têm número próprio.
Passo 1 — A fundação: infraestrutura de animação
Tudo depende deste passo — e ele já revela as decisões de arquitetura: o Lenis só entra em dispositivos sem touch (no celular, scroll nativo, como deve ser), os estados globais ganham um store com dono único, e o rastreador de mouse nasce aqui porque meia dúzia de motions vai bebê-lo da mesma fonte.
Em um projeto Next.js 14+ com App Router, TypeScript e Tailwind, configure a
infraestrutura de animação:
1. Instale: gsap @gsap/react lenis three matter-js
(+ @types/matter-js e @types/three em devDependencies)
2. Crie um componente client "SmoothScrollProvider" que:
- Instancia Lenis com { lerp: 0.1, smoothWheel: true } apenas em
dispositivos sem touch (matchMedia '(pointer: fine)'); em touch,
scroll nativo (mesma decisão do site auditado).
- Sincroniza com ScrollTrigger usando uma função nomeada:
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 Context (useLenis) para stop()/start()
(menu, preloader, zoom).
3. Registre plugins uma única vez em lib/gsap.ts:
gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP); exporte gsap.
Importado apenas por componentes client.
4. Crie o motor de progresso por seção (Motion 4): hook
useSectionProgress que escreve --pg-value em cada [data-progress].
5. Crie o rastreador global de mouse (Motion 4): pointermove com lerp
num rAF escrevendo --x e --y (normalizados de -1 a 1) no <html>.
6. Crie um pequeno store de estados globais (Zustand ou Context) que
espelha classes no <html>: --is-loading, --is-transition,
--is-scrolled, --is-menu-active, e o atributo
data-scroll-direction ('up' | 'down').
7. Hook usePrefersReducedMotion() lendo a media query
(prefers-reduced-motion: reduce); todo componente de animação deve
degradar quando true.
8. Tokens no globals.css (o site auditado usa beziers próprios):
:root {
--ease-soft-out: cubic-bezier(0.104, 0.204, 0.492, 1);
--ease-snap-out: cubic-bezier(0.306, 0.968, 0.632, 1);
--ease-in-out-strong: cubic-bezier(0.642, 0, 0.328, 1);
--dur-ui: 0.35s; --dur-reveal: 1s;
--x: 0; --y: 0; /* mouse global, escrito pelo rastreador */
}
Entregue os arquivos completos e o layout.tsx atualizado.Passo 2 — O motor duplo: --pg-value no scroll, --x/--y no mouse (Motion 4)
O coração da Camada 1, invisível por definição. Tudo que se move com o scroll lê o progresso da própria seção de uma variável; tudo que reage ao mouse lê a posição do ponteiro de outras duas, com atraso elástico. É por isso que os efeitos do site parecem todos irmãos: nascem das mesmas duas fontes.
Dois escritores, zero re-render de React. E dois detalhes de profissional: inicializar as vars no CSS (o HTML do servidor já sai no estado certo, sem flash) e escrever com parcimônia — toFixed(4) e um limiar mínimo de variação antes de tocar o DOM. Escrever var a cada frame sem mudança real é custo puro.
Crie o motor duplo de vars em Next.js:
1. Hook client "useSectionProgress(scopeRef)" dentro de useGSAP:
gsap.utils.toArray('[data-progress]', scope).forEach((el) => {
ScrollTrigger.create({
trigger: el,
start: el.dataset.start ?? 'top bottom',
end: el.dataset.end ?? 'bottom top',
scrub: 0.05,
onUpdate: (self) => el.style.setProperty(
'--pg-value', self.progress.toFixed(4))
});
});
Inicialize no CSS: :where([data-progress]) { --pg-value: 0 }
(o SSR renderiza o estado inicial sem flash).
2. Rastreador de mouse (client, no layout raiz):
- pointermove guarda o alvo normalizado
tx = (e.clientX / innerWidth) * 2 - 1 (idem ty);
- num gsap.ticker.add, faça x += (tx - x) * 0.08 e escreva
document.documentElement.style.setProperty('--x', x.toFixed(4))
(idem --y). Escreva só se a variação superar 0.0005.
- Desative em (pointer: coarse) e em prefers-reduced-motion
(congele --x/--y em 0).
3. Consumo no CSS (exemplos):
.col { transform: translateY(calc((1 - var(--pg-value)) *
var(--dist))); }
.clip { clip-path: inset(calc((1 - var(--pg-value)) * 15%)); }
.tilt { transform: rotateX(calc(var(--y) * -10deg))
rotateY(calc(var(--x) * 10deg)); }
4. will-change apenas nos elementos que animam, e togglado por
visibilidade: um IntersectionObserver marca data-inview="true" e o
CSS liga will-change só nesse estado (higiene de GPU idêntica à do
site auditado).
5. ScrollTrigger.refresh() após load de imagens/fontes e transições
de rota.Passo 3 — A tipografia que acende: scramble e sweep (Motion 5)
A assinatura tipográfica da ANAI não sobe nem desliza: ela acende. Cada letra do título começa numa cor apagada e vira a cor final em ordem aleatória, como lâmpadas de um letreiro ligando fora de sequência, até a palavra inteira estar acesa. Nos parágrafos, uma varredura: o bloco é revelado como se um marca-texto invisível passasse da esquerda para a direita.
Este é o coração da Camada 2 — e o passo instala também o observer central de seções, que todos os motions de entrada vão reutilizar (um único IntersectionObserver para a página inteira, não um por componente).
Crie o sistema de reveal tipográfico em Next.js:
1. Observer central: hook "useSectionObserver" com API
observer.add(sectionEl, callback) usando IntersectionObserver
(threshold 0.2, once por padrão); todos os motions de entrada se
registram nele (um único IO para a página inteira).
2. Componente "ScrambleTitle":
- SplitText.create(el, { type: 'chars', autoSplit: true,
onSplit: (self) => gsap.set(self.chars, { color: 'var(--col-dim)' }) })
(autoSplit aguarda as fontes; sem isso as quebras saem erradas);
- no callback do observer:
gsap.fromTo(split.chars, { color: cssVar('--col-dim') },
{ duration: 1, color: cssVar('--col-ink'), ease: 'power1.out',
stagger: { amount: 0.5, from: 'random' } });
- props from/to sobrescrevem as cores (útil sobre fundos escuros);
- aria-label com o texto no pai + aria-hidden nos chars.
3. Componente "SweepParagraph":
- CSS: color: transparent;
background-image: linear-gradient(90deg, var(--col-ink) 50%,
var(--col-dim) 50%);
background-size: 200% 100%; -webkit-background-clip: text;
background-clip: text; background-position: 100% 100%;
- no observer: gsap.to(el, { duration: 1,
backgroundPosition: '0% 0%', ease: 'power1.out' });
4. Complementos padrão de entrada de seção (mesmo callback):
gsap.fromTo(alvos, { autoAlpha: 0 }, { autoAlpha: 1,
duration: 0.5–1, delay escalonado 0/0.1/0.2 }).
5. prefers-reduced-motion: cores/fundos já no estado final, sem tween.Passo 4 — Estados globais: preloader, transições e header inteligente
Três motions que estabelecem --is-loading, --is-transition e --is-scrolled — as classes que o CSS inteiro referencia.
Motion 1 — Preloader com progresso e handoff coreografado
Antes de qualquer conteúdo, a tela mostra o logotipo centralizado e um contador discreto. Enquanto as imagens pesadas carregam, o número avança; ao chegar em 100, o preloader não simplesmente some: ele entrega o palco — o logo se recolhe, a cortina sai e o hero já entra animando. Carregamento e primeira cena viram uma única coreografia. (Visite o anaiwood.com e repare: é a primeira coisa que você vê, e é a capa deste post.)
O detalhe honesto: o progresso é real — Promises dos assets críticos, com o número interpolado via GSAP para nunca pular nem regredir. E o hero nunca anima antes do sinal do onComplete.
Crie um componente client "Preloader" em Next.js:
1. Overlay fixed inset-0 z-[999] com fundo sólido, logo central (SVG) e
contador (00 → 100). Enquanto ativo: classe '--is-loading' no <html>
e lenis.stop().
2. Progresso real: Promise.all parcial sobre os assets críticos
(document.fonts.ready + decode() das imagens above-the-fold);
cada asset resolvido incrementa a meta. Anime um objeto proxy com
gsap.to({ v }, { v: meta, duration: 0.6, ease: 'power1.out',
onUpdate }) para o número subir suave, sem saltos nem regressões.
3. Saída (timeline única):
- contador e logo: autoAlpha 0 + y sutil, 0.5s;
- painel do preloader: autoAlpha 0 (ou clip-path de cortina), 0.6s;
- onComplete: remove '--is-loading', chama lenis.start() e dispara
o evento/callback que inicia a entrada do hero (Motion 6); o
hero NUNCA anima antes desse sinal.
4. Rode uma vez por sessão (sessionStorage); nas demais visitas,
pule direto ao sinal de entrada do hero.
5. Acessibilidade: role="status" aria-live="polite" no contador;
prefers-reduced-motion encurta tudo para um fade rápido.Motion 2 — Transição de rota com máscara de escurecimento
Ao clicar num link, a página atual escurece sob um véu translúcido e desliza sutilmente, como se recuasse um passo; a nova entra por cima já disparando seus reveals. E um refinamento raro: indo "para frente" e "voltando", os sentidos do movimento se invertem — a hierarquia espacial da navegação fica legível no próprio motion.
Implemente transições de rota em Next.js App Router:
1. TransitionProvider (client) com navigate(href, direction):
- Seta '--is-transition' no <html> (CSS: .--is-transition .l-all
{ cursor: wait }) e pausa interações do conteúdo.
- Anima a saída da view atual: classe '--is-previous' +
gsap.to(viewAtual, { autoAlpha: 0.999, y: direction === 'forward'
? -24 : 24, duration: 0.45, ease: 'power1.inOut' }) em paralelo
com a máscara: div.transition-mask (absolute inset-0,
background rgba(0,0,0,0.5), pointer-events none) de opacity 0 → 1.
- Ao fim: router.push(href).
2. TransitionLink envolvendo next/link (prefetch preservado) que chama
navigate; links "voltar" passam direction 'backward'.
3. Entrada: template.tsx remonta a cada rota; nele remova
'--is-transition', faça lenis.scrollTo(0, { immediate: true }),
anime o container de opacity 0 → 1 (0.35s) e dispare os reveals de
entrada da página (Motion 5/6). ScrollTrigger.refresh() após as
imagens carregarem.
4. popstate (voltar do browser): pule a fase de saída e use apenas a
entrada.
5. prefers-reduced-motion: corte para fades curtos sem deslocamento.Motion 3 — Scroll suave + header inteligente por direção
Rolagem com inércia amanteigada no desktop (nativa no celular). E um cabeçalho esperto: rolando para baixo, ele se recolhe para devolver a tela ao conteúdo; ao primeiro gesto para cima, volta imediatamente. O site inteiro sabe em que direção você está indo — o Lenis entrega direction de graça, e o header reage só com CSS.
Implemente o header inteligente em Next.js:
1. No SmoothScrollProvider, assine lenis.on('scroll', ({ scroll,
direction }) => ...):
- scroll > LIMIAR (ex. 80px) → classe '--is-scrolled' no <html>;
abaixo do limiar, remova.
- direction (1 desce, -1 sobe) → atributo
data-scroll-direction="down"|"up" (atualize apenas quando mudar,
para não tocar o DOM a cada frame).
Em touch (sem Lenis), derive dos deltas de window.scrollY num
listener passive com rAF.
2. CSS do header (fixed top, z-index alto):
.header-title { transition: opacity var(--dur-ui) var(--ease-soft-out),
visibility var(--dur-ui), transform var(--dur-ui)
var(--ease-snap-out); }
.--is-scrolled[data-scroll-direction="down"] .header-title {
opacity: 0; visibility: hidden; pointer-events: none;
transform: translateY(calc(var(--header-h) / -10)); }
3. O header nunca se esconde com o menu aberto ('--is-menu-active'
tem precedência) nem durante '--is-transition'.
4. Âncoras internas: lenis.scrollTo(target, { offset: -headerH }).Passo 5 — Vitória rápida: underlines com origem trocada e setas que respiram (Motion 9)
Dois clássicos executados com precisão cirúrgica, 100% CSS. No sublinhado, o refinamento está na saída: ao tirar o mouse, a linha não recua — continua e sai pelo outro lado, como se atravessasse o link. O olho lê continuidade, não retorno. O truque é uma troca de transform-origin entre os estados. Nas setas, o ícone dispara para fora na direção que aponta e reentra por trás no mesmo instante — um respiro que confirma a direção do movimento.
Crie utilitários "u-underline" e "ArrowButton" em Next.js:
1. Underline com origem trocada (classe utilitária):
.u-underline { position: relative; }
.u-underline::before { content: ''; position: absolute; bottom: 0;
left: 0; width: 100%; height: 1px; background: currentColor;
transform: scaleX(0); transform-origin: 100% 50%;
transition: transform .4s var(--ease-soft-out); }
.u-underline:hover::before { transform: scaleX(1);
transform-origin: 0 50%; }
(a troca de origin faz a linha entrar pela esquerda e sair pela
direita; inverta os origins para o sentido oposto)
2. ArrowButton (setas de slider/links):
@keyframes arrow-out { to { transform: translateX(120%);
opacity: 0; } }
@keyframes arrow-in { from { transform: translateX(-120%);
opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.arrow-btn:hover .arrow-svg { animation: arrow-out .2s
var(--ease-in-out-strong) forwards,
arrow-in .3s .2s var(--ease-snap-out) forwards; }
Variante backward espelha os sinais; variante externa (links que
abrem fora) anima na diagonal (translate(120%, -120%)).
Contêiner com overflow: hidden.
3. Estados desabilitados das setas (fim de slider): opacity .3 +
pointer-events none.
4. focus-visible sempre com ring; hover só em @media (hover: hover).Passo 6 — O cartão de visitas: a janela que se abre e o lens flare
O hero valida o motor duplo do Passo 2 — as duas vars trabalhando juntas na mesma dobra.
Motion 6 — Hero: janela de imagem que se abre
O primeiro quadro é uma fotografia de atmosfera emoldurada por margens vazias — uma janela recortada no centro da tela. Sobre ela, o título se acende (Motion 5) e um convite de rolagem pulsa. Ao rolar, a janela se abre: as margens colapsam até a imagem ocupar tudo, enquanto a foto assenta de 105% para 100% de escala. A capa do livro abrindo para a primeira página.
Crie um componente client "HeroWindow" em Next.js:
1. Estrutura:
<section class="hero" data-progress data-start="top top"
data-end="bottom top"> <!-- height: ~180vh -->
<div class="hero-sticky"> <!-- sticky top-0 h-screen -->
<div class="hero-clip"> <!-- a janela -->
<img class="hero-img" ... />
</div>
<h1 class="hero-ttl">…ScrambleTitle em 2 linhas…</h1>
<div class="hero-scroll">…indicador de rolagem…</div>
</div>
</section>
2. CSS:
.hero-clip {
--clip-h: calc((1 - var(--pg-value)) * var(--clip-h-max, 12vh));
--clip-w: calc((1 - var(--pg-value)) * var(--clip-w-max, 10vw));
clip-path: inset(var(--clip-h) var(--clip-w));
height: 100svh;
}
.hero-img { transform: scale(calc(1.05 - var(--pg-value) * 0.05));
object-fit: cover; }
Espere: --pg-value 0 = janela fechada nas margens máximas;
1 = fullscreen. Ajuste os máximos por breakpoint.
3. Entrada pós-preloader (sinal do Motion 1): anime --clip-h-max/-w-max
de valores maiores (janela pequena) até os finais com gsap.to no
elemento (CSS vars são tweenáveis pelo GSAP), em paralelo com o
ScrambleTitle do h1 e o fade do indicador de rolagem.
4. Indicador de rolagem: rótulo + seta com o keyframe de seta do
Motion 9 em loop suave; some com o progresso
(opacity: calc(1 - var(--pg-value) * 2)).
5. Imagem com next/image (priority, sizes, placeholder blur usando a
versão preload desfocada).
6. prefers-reduced-motion: janela já aberta, sem scale.Motion 7 — Lens flare em camadas seguindo o mouse
Sobre a foto do hero vive um reflexo de lente: um disco de luz desfocado, um fragmento poligonal e um brilho terciário, como ao fotografar contra o sol. Ao mover o mouse, as camadas derivam em velocidades diferentes — exatamente como um flare real quando a câmera gira. E o custo? Zero JS próprio: são divs com blur e clip-path consumindo as vars globais de mouse com fatores diferentes. Sutil, quase subliminar — percebido antes de ser notado.
Crie um componente "LensFlare" em Next.js (Server Component: só CSS):
1. <div class="flare" aria-hidden="true"> com 3 sub-camadas absolutas
posicionadas sobre o ponto de luz da foto:
.flare-1 { width: 10vw; height: 10vw; border-radius: 50%;
background: rgb(64 64 64 / .1); filter: blur(2px);
transform: translate(-50%, calc(var(--x) * 15px)); z-index: 1; }
.flare-2 { width: 4vw; height: 4vw;
clip-path: polygon(50% 0, 90% 20%, 100% 60%, 75% 100%,
25% 100%, 0 60%, 10% 20%); /* fragmento heptagonal */
background: rgb(255 255 255 / .1);
transform: translate(-50%, calc(var(--x) * 25px)); z-index: 3; }
.flare-3 { …variação menor com fator 40px… }
2. Fatores crescentes por camada (15/25/40px) criam a deriva
diferencial; adicione var(--y) com fatores menores se quiser
deriva vertical também.
3. will-change: transform nas camadas; pointer-events: none no bloco.
4. Em (pointer: coarse) as vars ficam em 0 (Motion 4) e o flare fica
estático: comportamento correto sem código extra.
5. Mantenha opacidades baixas (≤ 0.12): o efeito deve ser percebido
antes de ser notado.Passo 7 — Produção em série: clips, parede em contra-parallax e rodapé
Três motions sobre a mesma base — quando a fundação está certa, cada seção nova custa minutos.
Motion 15 — Clip reveals de mídia por scroll
O vocabulário do hero reaparece em escala menor pelo site inteiro: imagens que entram abrindo suas molduras conforme a rolagem alcança cada uma. A consistência faz o site parecer feito de um material só — e é o mesmo par de sempre: clip-path + settle de scale, consumindo --pg-value local.
Crie um componente "ClipMedia" reutilizável em Next.js:
1. Props: as: 'img' | 'video', inset (percentual máximo, default 15%),
axis: 'both' | 'x' | 'y', range ({ start, end } do trigger).
2. Estrutura: <figure class="clip-media" data-progress
data-start="top bottom" data-end="center center">
<div class="clip"> <Image/afins class="media" /> </div> </figure>
3. CSS:
.clip { clip-path: inset(calc((1 - var(--pg-value)) *
var(--inset-max))); }
.media { transform: scale(calc(1.05 - var(--pg-value) * 0.05));
object-fit: cover; }
axis 'x' → inset(0 calc(...)); axis 'y' → inset(calc(...) 0).
4. Variante "GalleryFront": duas mídias empilhadas; a frontal usa
inset crescendo com o progresso (revela a de trás):
clip-path: inset(calc(var(--pg-value) * 100%) 0 0 0).
5. Vídeos: muted loop playsInline com pause fora do viewport.
6. Combine com legendas entrando pelo observer (Motion 5).
7. prefers-reduced-motion: inset 0 e scale 1 fixos.Motion 14 — Parede de colunas em contra-parallax
O fundo da seção é uma parede de colunas verticais de fotografias. Ao rolar, as colunas se movem em sentidos opostos: a primeira desce enquanto a segunda sobe, como engrenagens de um tear. O conteúdo flutua por cima. Zero JS próprio — só o sinal alternando por coluna no calc().
Crie um componente "ParallaxWall" em Next.js:
1. Estrutura: section[data-progress] (min-h: 130vh) >
div.wall (absolute inset-0, flex, justify-between, overflow
hidden) > N div.wall-col (cada uma com M imagens empilhadas) +
div.content (relative, z-index acima) com título (ScrambleTitle),
texto (SweepParagraph) e link.
2. CSS:
.wall-col { --dist: calc(var(--item-h) / 2);
transform: translateY(calc((1 - var(--pg-value)) * var(--dist)
* var(--dir))); will-change: transform; }
.wall-col:nth-child(odd) { --dir: 1;
margin-top: calc(var(--item-h) * -1); }
.wall-col:nth-child(even) { --dir: -1; }
Colunas com 1 item a mais que o necessário para nunca mostrar
borda vazia durante o percurso.
3. Véu de legibilidade entre a parede e o conteúdo
(background rgba escura ou gradiente) e data-theme="dark" na
seção para o cursor/header inverterem (Motions 3 e 11).
4. Imagens com next/image, sizes por coluna, loading lazy (a seção
fica abaixo da dobra) e placeholder blur.
5. Mobile: reduza para 2-3 colunas e metade da distância; em
prefers-reduced-motion, congele (--pg-value tratado no motor).Motion 18 — Rodapé com assentamento parallax
O rodapé chega assentando: enquanto a rolagem o revela, o bloco desliza uns centímetros mais devagar que a página, como uma bandeja encaixando no lugar, e só pousa quando o scroll termina. A distância é modesta de propósito — o efeito é de acomodação, não de cortina.
Crie um componente "SiteFooter" em Next.js:
1. Estrutura: footer > div.footer-inner[data-progress
data-start="top bottom" data-end="bottom bottom"] com os grupos
(endereço, colunas de navegação, redes, créditos/idiomas).
2. CSS:
.footer-inner { transform: translateY(calc((1 - var(--pg-value))
* var(--settle, 8rem))); will-change: transform; }
(em --pg-value: 1 o footer está pousado; o percurso curto do
trigger faz o assentamento coincidir com o fim da rolagem)
3. Links com .u-underline (Motion 9); e-mail/telefone como âncoras
reais; link externo com ArrowButton na variante diagonal.
4. Entradas internas discretas via observer (Motion 5): grupos com
autoAlpha + y: 16, stagger 0.06 (uma vez).
5. prefers-reduced-motion: --settle: 0.Passo 8 — Identidade de UI: o menu com pontos vivos e o cursor em duas camadas
Motion 10 — Menu fullscreen com botão de pontos "vivos"
O botão de menu é um círculo com três pontinhos que pulsam em onda, como se o botão respirasse. Clicou: um overlay escuro toma a tela e o conteúdo entra em camadas — links grandes, endereço, redes, cada grupo um instante depois do anterior. E o easter egg de arquitetura: quando a camada de física está ativa (Motion 17), os pontinhos abandonam o keyframe e ganham corpos físicos que reagem ao cursor.
Crie um componente client "MenuOverlay" em Next.js:
1. Botão fixo (canto superior): <button aria-expanded aria-controls>
com: div.dots > 3 span.dot; div.circle (fundo circular no hover);
div.labels (máscara com "Menu"/"Close" empilhados).
CSS dos dots:
@keyframes dot-pulse { 0%,100% { transform: translateY(0) }
50% { transform: translateY(-3px) } }
.dot { animation: dot-pulse .75s var(--ease-soft-out) infinite; }
.dot.--2 { animation-delay: .1s } .dot.--3 { animation-delay: .2s }
Quando a camada de física estiver ativa (Motion 17):
.--is-physics .dot { animation: none } (a simulação assume).
2. Rótulo flip: máscara overflow-hidden com 2 divs; aberto →
translateY(-100%) revela "Close".
3. Overlay: div fixed inset-0 z-50 pointer-events-none; com
'--is-menu-active' no <html>: fundo escuro fade .5s, conteúdo
opacity/visibility visíveis e pointer-events auto.
4. Conteúdo em 3 grupos (nav principal, contatos, idiomas/sociais)
entrando com gsap.fromTo autoAlpha + y: 24, stagger 0.08 entre
itens e delay 0.1 entre grupos; saída inversa rápida (0.3s).
5. Comportamento: Escape fecha; lenis.stop()/start(); focus trap com
retorno ao botão; troca de rota pelo menu fecha em paralelo à
transição (Motion 2); '--is-menu-active' impede o hide do header
(Motion 3).
6. prefers-reduced-motion: dots estáticos e overlay com fade simples.Motion 11 — Cursor customizado em duas camadas
Um marcador acompanha o ponteiro em duas camadas: a externa chega quase junto com o mouse; a interna assenta um instante depois, criando um rastro elástico de milímetros. Sobre áreas interativas, muda de estado (anel que cresce, ponto, cor invertida em fundos escuros). E a regra inegociável: ele nunca substitui o cursor do sistema — é um satélite.
Crie um componente client "MouseFollower" em Next.js:
1. Estrutura no layout raiz: div.mouse (fixed top-0 left-0, z-max,
pointer-events-none) > div.mouse-parent > div.mouse-child.
2. Movimento em camadas (useGSAP):
const px = gsap.quickTo('.mouse-parent', 'x',
{ duration: 0.25, ease: 'power2.out' }); // idem 'y'
const cx = gsap.quickTo('.mouse-child', 'x',
{ duration: 0.55, ease: 'power3.out' }); // idem 'y'
window pointermove → px/py e cx/cy com as mesmas coordenadas
(as durations diferentes criam o rastro).
3. Estados: .mouse-child::before (anel, scale 0→1) e ::after (ponto)
com transitions; delegação de mouseover no document lendo
closest('[data-cursor]') para classes '--link', '--drag',
'--view'; seções escuras marcam data-theme="dark" e o cursor
inverte a cor (color: currentColor).
4. Visibilidade: oculto até o primeiro pointermove; some ao sair da
janela (mouseleave no document) e durante '--is-transition'.
5. Renderize apenas em (hover: hover) and (pointer: fine); nunca use
cursor: none no site (o marcador é complemento, não substituto).
6. prefers-reduced-motion: uma camada única sem rastro.Passo 9 — O primeiro slider: vitrine com título central que troca (Motion 12)
Uma fileira de imagens e, presidindo a seção, um nome em letras grandes. Ao pairar sobre cada cartão ou avançar o slider, o nome central se troca — o atual apaga, o novo acende — junto com a descrição. A seção funciona como um mostruário cujo rótulo acompanha o que sua atenção toca. Na variante premium, a troca re-dispara o scramble do Motion 5 em vez de um fade simples.
Crie um componente client "ShowcaseSlider" em Next.js:
1. Dados: N itens { slug, title, description, image }. Layout:
fileira de cartões (overflow + translate por índice, ou Embla),
título central absoluto (h2.showcase-ttl) sobreposto à fileira e
pilha de descrições.
2. Swap do título (por hover no desktop, por slide ativo no touch):
- onEnter(item): gsap.to(ttl, { autoAlpha: 0, duration: 0.2,
onComplete: () => { ttl.textContent = item.title;
gsap.to(ttl, { autoAlpha: 1, duration: 0.3 }); } });
Variante premium: re-dispare o ScrambleTitle (Motion 5) no novo
texto em vez do fade.
- onLeave sem novo alvo: volte ao título default da seção.
3. Descrições: todas montadas empilhadas (absolute); a ativa com
opacity 1, demais 0 (transition .35s). aria-live="polite" na área.
4. Navegação: setas ArrowButton (Motion 9) com estados desabilitados
nas extremidades; arraste opcional (pointer events → deltaX no
translate, com snap ao índice mais próximo no release).
5. Imagens dos cartões: moldura overflow-hidden com hover zoom
(scale 1.05, .6s) e clip sutil de entrada via observer (Motion 5).
6. Sincronize hover ↔ slide: hover marca o item "em foco" sem mover o
slider; setas movem o slider e atualizam o título pelo item central.Passo 10 — A joia da coroa: slider sincronizado com anéis em canvas (Motion 13)
O momento mais autoral do site. Ao lado da lista de projetos, um desenho de anéis concêntricos — alusão direta aos anéis de crescimento da madeira que é a matéria-prima da ANAI — vive num canvas 2D. A cada avanço do slider, os anéis giram meia volta com easing, como se o tempo da matéria acompanhasse a navegação; o número da página troca com um flip de dígito. Conteúdo, contador e desenho pulsam juntos: navegação vira ritual.
É a Camada 3 em seu melhor: o canvas não sabe nada do slider — ele só desenha em função de um número contínuo (frames), e o slider tweena esse número. Acoplamento mínimo, expressividade máxima.
Crie um componente client "RingsSlider" em Next.js:
1. Classe "Rings" (canvas 2D):
- new Rings({ elem, color, steps, base, outer, padding }) cria um
<canvas> responsivo (resize observer, dpr) dentro de elem;
- draw(frames): limpa e desenha os anéis concêntricos; use frames
para modular o desenho (raio de fase, rotação de um marcador ou
espessura alternada; parametrize a interpretação);
- update() força um redraw (chamado quando a seção entra em view).
Responsivo: menos anéis/raio menor em mobile (props por breakpoint).
2. Estado compartilhado: const frames = { current: 0, next: 0 }.
Ao trocar de slide (setas, arraste ou clique num item):
frames.next = frames.current + 0.5;
gsap.killTweensOf(frames);
gsap.to(frames, { duration: 0.25, current: frames.next,
ease: 'power1.out', onUpdate: () => rings.draw(frames.current) });
3. Slider de conteúdo: painéis de texto/imagem trocando por índice
(translate ou crossfade); título do item com ScrambleTitle
(Motion 5) a cada troca.
4. Paginação com flip de dígito:
<span class="nums"> (1em x 1em, overflow hidden) com duas camadas
.num-child empilhadas; na troca: a atual ganha '--is-hide'
(translateY(-100%) + fade) e a nova '--is-show' entrando de baixo;
ao fim, troque os papéis (sempre só 2 nós).
5. Acessibilidade: canvas aria-hidden (decorativo); slider com
role="group", aria-roledescription="carousel", botões rotulados;
contador com aria-live="polite".
6. Pause: só desenhe com a seção no viewport (IntersectionObserver);
cleanup de canvas/observers no unmount.
7. prefers-reduced-motion: anéis estáticos (draw único) e troca de
slide por fade.Passo 11 — Polish rápido: tilt 3D de imagens pelo mouse (Motion 8)
Certas fotografias inclinam-se sutilmente na direção do mouse, como cartões suspensos reagindo à sua presença. Nada persegue o cursor: tudo apenas se curva a ele. E aqui a arquitetura paga o investimento do Passo 2: como o lerp mora no rastreador global, N instâncias de tilt custam o mesmo que uma — nenhum listener local, só CSS lendo as vars.
Crie um componente "TiltImage" em Next.js:
1. Estrutura: <figure class="tilt-wrap"> (perspective: 1000px)
> <div class="tilt" style="--rx: 10deg; --ry: -10deg">
> <img class="tilt-img">.
2. CSS:
.tilt { transform: rotateX(calc(var(--y) * var(--ry)))
rotateY(calc(var(--x) * var(--rx)));
will-change: transform; }
.tilt-img { transition: transform .6s var(--ease-soft-out); }
.tilt-wrap:hover .tilt-img { transform: scale(1.05); }
Opcional: overlay ::before com opacity 0 → 1 no hover (véu).
3. Amplitudes como props (--rx/--ry por instância); inverta os sinais
para "fugir" ou "seguir" o mouse conforme a direção desejada.
4. A suavidade vem do lerp do rastreador global (Motion 4): sem
listeners locais, N instâncias custam o mesmo que uma.
5. Em touch/reduced-motion as vars ficam em 0: imagem estática com
hover-scale apenas onde houver hover real (@media (hover: hover)).Passo 12 — As camadas "wow": partículas WebGL e física DOM
As duas últimas camadas são açúcar puro — e é exatamente assim que devem ser tratadas: o site precisa funcionar idêntico sem elas. Em reduced motion ou hardware fraco, elas simplesmente não montam.
Motion 16 — Partículas ambientais WebGL
Sobre a fotografia, partículas minúsculas flutuam no ar: poeira ao sol, cada uma com tamanho, brilho e ritmo próprios, subindo e derivando, esmaecendo ao longe. Quase invisível quando se olha direto — e é exatamente o que faz a cena parecer ar em vez de papel de parede.
Crie um componente client "AmbientParticles" em Next.js com Three.js:
1. Wrapper "GLCanvas": WebGLRenderer({ alpha: true, antialias: true }),
setPixelRatio(min(devicePixelRatio, 2)), canvas absoluto sobre a
mídia (pointer-events none), resize via ResizeObserver.
2. Geometria: BufferGeometry com COUNT (300-800) partículas:
position (x/y aleatórios no volume, z em faixas), e atributos
aProgress, aSize, aAlpha (Math.random() por partícula).
3. Vertex shader: posição deslocada por simplex noise 3D
(freq/multiplier em uniforms) + avanço vertical por
(aProgress + iTime * uSpeed) mod 1; gl_PointSize proporcional a
aSize e à profundidade; varying vAlpha.
4. Fragment shader: disco suave (smoothstep radial) * vAlpha * fog.
5. Uniforms: iTime (frames), iMouse (lido do rastreador global do
Motion 4 para deriva sutil), iResolution, uSize, uNoiseFrequency,
uNoiseMultiplier, uProgressSpeed (todos como props).
6. Material: transparent, blending: THREE.AdditiveBlending,
depthWrite: false.
7. Loop: gsap.ticker (função nomeada); renderize apenas com o canvas
no viewport (IntersectionObserver). Cleanup: ticker.remove,
geometry/material.dispose(), renderer.dispose().
8. Fallback: sem WebGL ou em prefers-reduced-motion, não monte
(a cena vive sem as partículas).Motion 17 — Física de elementos DOM (Matter.js)
Alguns elementos da interface têm peso de verdade: soltos num recipiente invisível, caem, quicam nas bordas e se acomodam; aproximar o cursor os empurra fisicamente, como moedas numa bandeja. Na ANAI, os pontinhos do botão de menu ganham essa vida quando a camada avançada está ativa — deixam o pulso coreografado e passam a reagir ao seu mouse.
Crie um sistema de física DOM em Next.js com Matter.js:
1. Classe "PhysicsWorld({ container, gravity })":
- Engine.create(); walls: 4 Bodies.rectangle estáticos nas bordas
do container (espessura generosa, invisíveis);
- cursor: Bodies.circle estático (raio ~24px) atualizado no
pointermove (Body.setPosition), colide e empurra os corpos;
- runner próprio via gsap.ticker (função nomeada) chamando
Engine.update(engine, 1000/60).
2. Classe "PhysicsItem({ el, shape: 'rect' | 'circle' })":
- mede o el (getBoundingClientRect relativo ao container) e cria o
corpo com restitution .6, friction .1;
- update(): lê position/angle do corpo e aplica no el
transform: translate(xpx, ypx) rotate(deg) (arredonde a 2 casas;
o el fica position: absolute no estado inicial medido).
3. Aplicação exemplo (pontos do botão de menu): ao ativar a camada
('--is-physics' no <html>), os dots trocam o keyframe de pulso
(animation: none) pelos corpos físicos dentro do círculo do botão;
ao desativar, destrua o mundo e restaure o keyframe.
4. Resize: recrie paredes e reposicione corpos (debounce).
5. Cleanup completo: ticker.remove, Composite.clear, Engine.clear,
listeners removidos.
6. Física é açúcar: em touch de baixo desempenho e em
prefers-reduced-motion, mantenha a versão coreografada em CSS.As 18 armadilhas que vão te morder (leia antes de codar)
A lista cresceu em relação ao guia anterior — canvas e física trazem armadilhas próprias. Passe por ela antes da primeira linha, e volte quando algo quebrar "misteriosamente".
"use client"obrigatório em todo componente que toca GSAP, Lenis, Three, Matter ou observers.- Sempre
useGSAP({ scope })em vez deuseEffectcru: ocontext.revert()automático mata tweens e triggers órfãos ao navegar entre rotas. - Cleanup do Lenis é em 3 passos:
gsap.ticker.remove(update)+lenis.off('scroll', ...)+lenis.destroy(). - Trava de scroll com Lenis:
lenis.stop()/start()(preloader, menu), nunca apenasoverflow: hidden. - CSS vars + SSR: inicialize
--pg-value: 0e--x/--y: 0no CSS (não só via JS) para o primeiro paint do servidor já sair correto. - Vars de mouse no
<html>são globais de propósito: N consumidores (flare, tilt, cursor, partículas) leem as mesmas duas vars; nunca crie um listener de pointermove por componente. - Escreva vars com parcimônia:
toFixed(4)e um limiar mínimo de variação antes de tocar o DOM; escrever a cada frame sem mudança real é custo puro. - Só
transform,opacityeclip-pathnas animações por CSS var; nunca width, height, top ou left (layout thrashing). will-changetogglado por visibilidade (atributodata-inviewvia IntersectionObserver), exatamente como o site auditado:will-changepermanente degrada a memória de GPU.ScrollTrigger.refresh()após load de imagens e fontes e após cada transição de rota.- SplitText só depois das fontes (
autoSplit: trueoudocument.fonts.ready);aria-labelno pai +aria-hiddennos chars, ou leitores de tela soletram o título. gsap.matchMedia()para variações responsivas;ScrollTrigger.matchMediaestá deprecado desde o GSAP 3.11.- Estados globais têm um dono só:
--is-loading,--is-transition,--is-scrolled,--is-menu-activeedata-scroll-directionvivem num store central que espelha classes no<html>; toggles espalhados criam estados fantasma. - Canvas sempre com dpr limitado (
min(devicePixelRatio, 2)), pausa fora do viewport e dispose completo (Three: geometry/material/renderer; Matter: Composite/Engine; 2D: observers). - Cursor, tilt e flare: desktop apenas (
(hover: hover) and (pointer: fine)); em touch as vars ficam em 0 e os componentes degradam sozinhos. Nuncacursor: none. - Física e partículas são açúcar: o site precisa funcionar idêntico sem elas (fallback coreografado em CSS, camada não montada em reduced motion ou hardware fraco).
- O preloader precisa de progresso real: contador fake que trava em 99 esperando um asset é o anti-padrão; some Promises de verdade e interpole com GSAP.
prefers-reduced-motionem tudo: preloader vira fade, janela do hero nasce aberta, scramble vira cor final, sliders trocam por fade.
Por onde começar hoje
O trio de entrada é o mesmo espírito do guia anterior: Passo 1 (a fundação), Passo 2 (o motor duplo — que aqui rende dobrado, porque alimenta scroll E mouse) e Passo 5 (underlines e setas — CSS puro, resultado imediato). Com o Passo 3 logo depois, você tem a assinatura tipográfica que muda a cara de qualquer página.
E fica o convite ao estudo comparado: abra o anaiwood.com e o site da Plantica lado a lado, com os dois guias abertos. Uma marca de madeira e um estúdio floral, uma arquitetura híbrida de três camadas e uma CSS-first radical — e por baixo, os mesmos princípios: variáveis para o contínuo, tweens para os eventos, canvas para o espetáculo, e acessibilidade como fundação. É esse repertório de decisões — não os efeitos em si — que você leva para o próximo projeto.
Leia também
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
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


