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

Sumário
- A filosofia: GSAP mínimo, CSS máximo, estados por seção
- A stack e o papel de cada peça
- O mapa dos 16 motions
- Passo 1 — A fundação: Lenis, o hook de entrada e o split no render
- Passo 2 — O sistema nervoso tipográfico: reveal de palavras por seção (Motion 3)
- Passo 3 — Vitórias rápidas em CSS puro: underline que viaja e marquee com máscara
- Passo 4 — A moldura de entrada: preloader com Lottie e a pincelada de rota
- Passo 5 — A casca de navegação: header camaleão e nav mobile em cascata
- Passo 6 — O padrão em profundidade: acordeão com palavras em cascata (Motion 13)
- Passo 7 — O par de autoplay: depoimentos com anel e citação que se reescreve
- Passo 8 — A seção mais viva: processo autoplay com cortinas e cor viva (Motion 8)
- Passo 9 — O único pin do site: cards com tombamento 3D (Motion 9)
- Passo 10 — Interações de conteúdo: abas com saída limpa e a calculadora viva
- Passo 11 — O fechamento: formas de marca e rodapé parallax
- As 15 armadilhas que vão te morder (leia antes de codar)
- Por onde começar hoje
Quarto estudo de engenharia reversa da série, e mais uma resposta diferente para a mesma pergunta. A Plantica animava tudo com variáveis contínuas de CSS; a ANAI dividia o trabalho em três camadas; o Studio Modular empacotava cada efeito num módulo com gates. O site desta vez tem a filosofia mais enxuta de todas — e a que melhor conversa com o jeito React de pensar.
O auditado é a Beetogreen (beetogreen.com), líder francesa em mobilidade corporativa por bicicleta — "Mobility that makes a difference", mais de 500 mil funcionários elegíveis, 500+ empresas atendidas. Visualmente: verde-limão elétrico sobre verde-floresta, formas orgânicas gigantes recortando o vídeo do hero, e uma transição de rota assinada com uma pincelada caligráfica. Por trás, uma plataforma Nuxt/Vue + CMS headless onde o JavaScript de animação se resume a meia dúzia de intervenções cirúrgicas — todo o resto é CSS disparado por classes de estado. Os 16 padrões abaixo foram catalogados auditando esse site e traduzidos para o ecossistema Next.js; textos, cores e nomes são placeholders. O que você leva é o padrão, a técnica e um prompt pronto para colar no seu agente de IA.
Como sempre, a ordem dos passos é a ordem de construção.
A filosofia: GSAP mínimo, CSS máximo, estados por seção
O contrato de animação da Beetogreen cabe numa frase: o JS decide "em que estado a seção está"; o CSS decide "como cada estado se parece". Na prática:
- O GSAP aparece em pouquíssimos lugares: preloader, transição de rota e dois scrubs (os cards 3D e o rodapé). Só.
- Todo o resto é CSS disparado por classes de estado no nível da seção:
--entered,--active,--past,--open. Um único hook de IntersectionObserver alterna a entrada; a lógica de autoplay alterna o resto. - O trio
--active/--past/--futureé o que permite trocas de conteúdo em fluxo contínuo: as palavras do conteúdo velho sobem e saem por cima, as do novo entram por baixo — mesma fenda, sem brigas. - As cores das seções vêm do CMS e as animações as respeitam: o valor mora no
styleinline, atransitionmora no CSS. A seção muda de paleta sem uma linha de tween. - E o padrão mais React-idiomático da série: replay por chave. Keyframes CSS re-executam quando o nó remonta (
key={n}) — é assim que citações se reescrevem, anéis de autoplay reiniciam e formas de marca re-entram. A chave é o botão de play.
A stack e o papel de cada peça
| Biblioteca | Papel no projeto | Por quê |
|---|---|---|
| Lenis | Smooth scroll com easing expo custom + scrollTo programático | O site auditado integra Lenis ao ScrollTrigger e expõe isScrolling num store global |
| GSAP 3.13+ e @gsap/react | Preloader, transição de rota "pincelada", dois scrubs (cards 3D e rodapé) | GSAP é 100% gratuito, incluindo todos os plugins, desde a aquisição pela Webflow. Aqui é usado de forma minimalista |
| ScrollTrigger (plugin GSAP) | Pin dos cards de estatísticas e parallax do rodapé (com clamp()) | Únicos dois usos de scroll-drive do site inteiro |
Lottie (@lottiefiles/dotlottie-react) | Logo animado do preloader | A duração do próprio Lottie temporiza a cortina de saída |
| CSS transitions, keyframes e classes de estado | Todo o resto: reveals de palavra, cortinas de imagem, acordeão, marquee, anéis de progresso | Roda no compositor; os estados são a única API |
Nenhum plugin de carrossel: o slider de depoimentos é próprio (troca por índice + replay de keyframes por remontagem de chave — Motion 11).
O mapa dos 16 motions
| # | Motion | Uso típico | Gatilho |
|---|---|---|---|
| 1 | Preloader com logo animado + cortina | Primeira carga | Load |
| 2 | Transição de rota "pincelada" | Toda navegação | Navegação |
| 3 | Reveal de palavras por seção | Títulos e parágrafos, o site inteiro | Inview |
| 4 | Underline que se apaga + micro-links | Links CTA de texto | Hover |
| 5 | Marquee parametrizado com máscara | Faixas de logos/palavras | Loop |
| 6 | Header adaptativo por tema + véu | Navegação global | Scroll/Hover |
| 7 | Nav mobile com cascata de tags | Menu mobile | Clique |
| 8 | Processo autoplay com cortinas e cor viva | Seção "como funciona" | Inview/Auto/Clique |
| 9 | Cards pinados com tombamento 3D | Estatísticas/manifesto | Scroll |
| 10 | Depoimentos com anel de contagem | Prova social | Auto/Clique |
| 11 | Citação que se reescreve palavra a palavra | Dentro do Motion 10 | Troca de slide |
| 12 | Cards em abas com transição de saída/entrada | Vantagens/planos | Clique |
| 13 | Acordeão com palavras em cascata | FAQ | Clique |
| 14 | Calculadora interativa com resultados vivos | Simulador/orçamento | Input |
| 15 | Formas decorativas de marca por seção | Fundos e apoios visuais | Estado |
| 16 | Rodapé parallax + voltar ao topo suave | Rodapé global | Scroll/Clique |
Reveals genéricos de seção são sempre o Motion 3: não existe outro sistema de entrada no site auditado — a consistência vem daí.
Passo 1 — A fundação: Lenis, o hook de entrada e o split no render
A infra deste guia tem duas peças que merecem atenção especial. O useHasEntered — um IntersectionObserver once que devolve um boolean — é o único gatilho de reveal do site inteiro. E o splitWords quebra os textos em spans durante o render (util puro, server-friendly): o markup já chega ao browser com as fendas prontas, sem medição de DOM, sem flash, sem mismatch de hidrataçã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 @lottiefiles/dotlottie-react
2. Crie um componente client "SmoothScrollProvider" que:
- Instancia Lenis com o easing do site auditado:
new Lenis({
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -8 * t)),
orientation: 'vertical',
smoothWheel: true
})
- Sincroniza com ScrollTrigger usando 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);
lenis.off('scroll', ScrollTrigger.update);
lenis.destroy();
- Expõe via Context (useLenis) a instância + um estado isScrolling
(do callback de scroll do Lenis), como faz o site auditado.
3. Registre plugins uma única vez em lib/gsap.ts:
gsap.registerPlugin(ScrollTrigger, useGSAP); exporte gsap.
4. Crie o hook "useHasEntered(ref, { threshold })": IntersectionObserver
once que retorna hasEntered (boolean). É o ÚNICO gatilho de reveal
do site inteiro: cada seção o usa para ligar sua classe '--entered'.
5. Crie um util "splitWords(texto)" (e "splitLinesAndWords") que quebra
strings em arrays de palavras no RENDER (server-friendly): o markup
já chega ao browser com os spans de reveal prontos, sem medição.
6. Hook usePrefersReducedMotion(); componentes degradam quando true.
7. Tokens no globals.css (easings do site auditado):
:root {
--ease-reveal: cubic-bezier(0.77, 0, 0.175, 1);
--ease-ui: cubic-bezier(0.645, 0.045, 0.355, 1);
--ease-soft: cubic-bezier(0.32, 0.72, 0, 1);
--dur-reveal: 1s;
}
Entregue os arquivos completos e o layout.tsx atualizado.Passo 2 — O sistema nervoso tipográfico: reveal de palavras por seção (Motion 3)
Nenhum texto da Beetogreen aparece de uma vez: cada palavra sobe de dentro de uma fenda invisível, uma fração de segundo depois da anterior, num movimento firme e desacelerado. E quando um conteúdo sai de cena (troca de step, de slide), as palavras sobem e desaparecem pela borda de cima, abrindo espaço para as novas subirem por baixo. O site inteiro respira nesse compasso — quase todos os motions seguintes consomem este.
A arquitetura importa tanto quanto o efeito: o gatilho é uma classe na seção (nunca por palavra), e os três estados — futuro embaixo, visível no lugar, passado em cima — são o que permite trocas de conteúdo em fluxo contínuo nos Motions 8, 10 e 13.
Crie o sistema de reveal de palavras em Next.js:
1. Util puro "splitWords(text): string[]" e componente
"RevealWords({ text, as, delay = 0, step = 0.04 })" que renderiza:
<Tag aria-label={text}>
{words.map((w, i) =>
<span class="w" aria-hidden="true" key={i}>
<span class="w-inner"
style={{ '--d': `${delay + i * step}s` }}>{w}</span>
</span>)}
</Tag>
(split no render = SSR perfeito, zero medição, zero flash)
2. CSS:
.w { display: inline-block; overflow: hidden;
vertical-align: bottom; margin-right: .1em;
padding-bottom: .12em; } /* respiro p/ descendentes (g, p) */
.w-inner { display: inline-block; transform: translateY(110%);
transition: transform var(--dur-reveal) var(--ease-reveal);
transition-delay: var(--d, 0s); }
[data-entered="true"] .w-inner,
.--active .w-inner { transform: translateY(0); }
.--past .w-inner { transform: translateY(-110%); }
(o estado "passado" sai por cima: é o que permite trocas de
conteúdo em fluxo contínuo nos Motions 8, 10 e 13)
3. Gatilho por seção: cada section usa useHasEntered(ref,
{ threshold: 0.2 }) e escreve data-entered no próprio nó. Um
único IO por seção, nunca por palavra.
4. Variante "AnimatedParagraph": mesmo padrão com step menor (0.01)
para parágrafos longos.
5. prefers-reduced-motion: .w-inner sem transition (estado final).Passo 3 — Vitórias rápidas em CSS puro: underline que viaja e marquee com máscara
Motion 4 — Underline que se apaga + micro-interações de link
Os links de texto vêm já sublinhados. No hover, o sublinhado se apaga escorrendo para a direita; ao sair, volta entrando pela esquerda. A linha nunca cresce e encolhe no mesmo lugar — ela viaja. É a mesma troca de transform-origin do Studio Modular, invertida: lá a linha se desenhava no hover; aqui ela se apaga. Detalhe de personalidade.
Crie utilitários "u-underline-out" e estados de botão em Next.js:
1. Underline que se apaga:
.u-underline-out { position: relative; display: inline-block;
overflow: hidden; vertical-align: top; }
.u-underline-out::after { content: ''; position: absolute;
bottom: 0; left: 0; width: 100%; height: 1px;
background: currentColor; transform: scaleX(1);
transform-origin: left center;
transition: transform .7s var(--ease-reveal); }
.u-underline-out:hover::after { transform: scaleX(0);
transform-origin: right center; }
(repouso: linha cheia; hover: apaga saindo pela direita;
unhover: redesenha entrando pela esquerda)
2. Botão com loading:
.button[data-loading="true"] .button__label { opacity: 0; }
.button__spinner { position: absolute; inset: 0; margin: auto;
width: 1.2em; aspect-ratio: 1; border-radius: 50%;
border: 2px solid currentColor;
border-right-color: transparent;
animation: spin 0.8s linear infinite; opacity: 0; }
.button[data-loading="true"] .button__spinner { opacity: 1; }
@keyframes spin { to { transform: rotate(1turn) } }
aria-busy="true" junto com data-loading.
3. Badge opcional no botão (contadores/etiquetas) como span
posicionado, sem animação própria.
4. Hovers só em @media (hover: hover); focus-visible com ring.Motion 5 — Marquee parametrizado com máscara de fade
Uma esteira de logotipos desliza continuamente — com dois refinamentos que a separam do marquee comum: as bordas se desmancham em fade (os logos nascem e morrem suavemente nas laterais, via mask-image), e cada instância escolhe velocidade e sentido próprios por CSS vars. Server Component, zero JS.
Crie um componente "FadeMarquee" em Next.js (Server Component):
1. Props: duration ('30s'), direction ('normal' | 'reverse'),
gap ('6rem'), itemHeight ('3.2rem'), tint ('none' | 'dark' |
'light'), fadeEdges (true), children (itens).
2. Estrutura: div.marquee (overflow hidden, width 100%,
style com as CSS vars) > ul.marquee__list (inline-flex, gap,
white-space nowrap, will-change transform, class "mq") com os
itens renderizados DUAS vezes (aria-hidden na segunda cópia).
3. CSS:
.mq { animation: marquee var(--marquee-duration) linear infinite;
animation-direction: var(--marquee-direction); }
@keyframes marquee { from { transform: translate(0) }
to { transform: translate(-50%) } }
.marquee--fade { mask-image: linear-gradient(90deg,
transparent 0, #000 8%, #000 92%, transparent);
-webkit-mask-image: /* idem */; }
.marquee--tint-dark .logo { filter: brightness(0); }
.marquee--tint-light .logo { filter: brightness(0) invert(1); }
4. A matemática do -50% exige as duas cópias idênticas; gap incluído
dentro da cópia (padding-right no último item ou gap no flex com
cópia começando alinhada).
5. Pausa opcional no hover; prefers-reduced-motion: animation none
(faixa estática com overflow-x auto para ainda ser explorável).Passo 4 — A moldura de entrada: preloader com Lottie e a pincelada de rota
Motion 1 — Preloader com logo animado + cortina
A tela abre num painel de cor sólida onde o logotipo se desenha sozinho (Lottie). Antes mesmo de a animação terminar, o painel sobe como cortina de palco, revelando o site pronto por trás. O detalhe de engenharia: o tempo da cortina é calculado da duração do próprio Lottie — nada é chute. E o scroll é liberado no meio da cortina, não no fim.
Crie um componente client "Preloader" em Next.js:
1. Painel fixed inset-0 z-[60] com background sólido (token) e o
<DotLottieReact> do logo centralizado (autoplay, sem loop),
inicialmente opacity 0.
2. Fluxo (useGSAP + useLenis):
- onMounted: lenis.stop();
- quando o player estiver pronto: gsap.to(lottieWrap,
{ opacity: 1, duration: 0.1, ease: 'power2.out' });
const dur = player.getDuration(); // segundos
agende a cortina em dur * WIPE_RATIO (const, ex. 0.6);
fallback: se a duração não vier, dispare imediatamente.
- Cortina: tl = gsap.timeline({ delay });
tl.to(panel, { clipPath: 'inset(0 0 100% 0)', duration: 1.2,
ease: 'expo.inOut' });
tl.call(() => { lenis.start(); onPreloaderDone(); }, [], 0.5);
// libera scroll e reveals NO MEIO da cortina, não no fim
tl.call(() => removePanel());
3. onPreloaderDone() é o sinal que o hero (Motion 3) espera para
ligar '--entered' na primeira dobra.
4. Rode uma vez por sessão (sessionStorage); demais visitas: sem
painel, sinal imediato.
5. prefers-reduced-motion: painel com fade rápido, sem Lottie.Motion 2 — Transição de rota "pincelada"
A assinatura do site. Ao clicar num link, uma pincelada atravessa a tela: um traço orgânico começa fino, corre pelo caminho de um gesto desenhado e vai engordando até virar uma mancha que cobre tudo. A página troca por trás da tinta; o traço então se desfaz no sentido inverso. A cor da tinta pode mudar conforme o destino — uma transição com caligrafia, não um fade genérico.
A mecânica é um só <path> SVG: stroke-dashoffset desenha o gesto enquanto stroke-width engorda de 450 até 800, cobrindo a tela. E as regras de robustez valem ouro: Promises que resolvem uma única vez, timeline anterior sempre morta antes da nova.
Crie o sistema de transição de rota em Next.js:
1. Componente "TransitionOverlay" (client, no layout raiz):
div fixed inset-0 z-50 pointer-events-none visibility-hidden >
svg (viewBox cobrindo a tela, scale 1.1, preserveAspectRatio
"xMidYMid slice") > path do gesto (fill none, stroke currentColor,
stroke-linecap round). Desenhe um path em S/diagonal que atravesse
o viewBox; exponha setStrokeColor(cor).
2. API imperativa (Context): prepare(), playOut(cor), playIn():
- prepare(): const len = path.getTotalLength();
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len,
opacity: 0 }); gsap.set(svg, { scale: 1.1 });
- playOut(cor): Promise que:
seta a cor; gsap.set(wrapper, { pointerEvents: 'auto',
visibility: 'visible', opacity: 1 });
tl = gsap.timeline({ defaults: { duration: .7,
ease: 'power1.inOut' } });
tl.to(path, { opacity: 1, duration: .05 });
tl.to(path, { strokeDashoffset: 0,
attr: { 'stroke-width': 450 } }, '<');
tl.call(() => { gsap.set(path, { strokeDashoffset: 0,
attr: { 'stroke-width': 800 } }); resolve(); }, [], '>-0.2');
// o salto final para 800 garante cobertura total sem custo
- playIn(): Promise inversa (defaults .9s):
ao completar, gsap.set(wrapper, { pointerEvents: 'none',
visibility: 'hidden', opacity: 0 }) e reset do path
(dashoffset = len, stroke-width 2, opacity 0).
- SEMPRE mate a timeline anterior (tl?.kill()) antes de criar
outra, e resolva a Promise uma única vez (flag).
3. "TransitionLink": intercepta o clique → await playOut(corDoDestino)
→ router.push → no template.tsx da nova rota, useEffect chama
playIn() (e lenis.scrollTo(0, { immediate: true }) antes).
4. popstate: pule playOut; rode só playIn curto.
5. prefers-reduced-motion: substitua por fade de 0.3s no wrapper.Passo 5 — A casca de navegação: header camaleão e nav mobile em cascata
Motion 6 — Header adaptativo por tema + véu de dropdown
O cabeçalho é uma cápsula fixa que muda de pele conforme a seção: branco sobre fundos claros, verde-escuro sobre seções escuras, sem quebra. O truque de arquitetura: o header consome apenas tokens (--nav-bg, --nav-text) e as variantes só trocam os tokens — a mudança de pele é uma classe. E ao abrir um submenu, um véu escurece o resto da página, focando a atenção.
Crie um componente client "AdaptiveHeader" em Next.js:
1. header fixed top-0 z-40 com container-cápsula lendo tokens:
.nav { --nav-bg: #fff; --nav-text: var(--tk-ink); }
.nav--dark { --nav-bg: var(--tk-ink); --nav-text: #fafafa; }
.nav--accent { --nav-bg: var(--tk-accent); --nav-text: #fafafa; }
.nav__container { background: var(--nav-bg); color: var(--nav-text);
transition: background .35s var(--ease-soft),
color .35s var(--ease-soft); border-radius: 999px; }
2. Tema por seção: seções marcam data-nav-theme="dark|light|accent";
um IO com rootMargin "-50% 0px -50% 0px" (linha do meio) observa
e seta a variante do header conforme a seção sob o topo.
3. Dropdown desktop: ao abrir (hover/click com aria-expanded),
renderize o painel dentro da cápsula e ligue o véu:
.nav__overlay { position: fixed; inset: 0;
background: rgb(0 0 0 / .3); opacity: 0; pointer-events: none;
transition: opacity .35s var(--ease-soft); z-index: 0; }
.nav__overlay--visible { opacity: 1; }
Clique no véu fecha o dropdown; Escape idem.
4. O container do header fica acima do véu (z-index 1) e o conteúdo
da página abaixo: o véu foca sem bloquear o próprio menu.
5. Lang switcher como dropdown compacto com a mesma mecânica.Motion 7 — Nav mobile com cascata de tags
O menu mobile abre e os itens não chegam juntos: cada link sobe um degrau com fade, um após o outro, como etiquetas sendo penduradas em sequência. E um detalhe de implementação que ensina o padrão central do site: keyframes, não transitions — porque keyframes re-executam quando o overlay remonta, dando replay limpo a cada abertura.
Crie um componente client "MobileNav" em Next.js:
1. Botão hambúrguer (aria-expanded, aria-controls) + overlay
fullscreen (montado condicionalmente para o replay dos keyframes).
2. CSS:
@keyframes tag-reveal { from { opacity: 0;
transform: translateY(50%); } to { opacity: 1;
transform: translateY(0); } }
.mnav__item { opacity: 0;
animation: tag-reveal .45s var(--ease-ui) forwards;
animation-delay: calc(.15s + var(--i) * .06s); }
.mnav__social { /* mesmo keyframe, delays após os itens */ }
Passe --i por style inline no map dos itens.
3. Fundo do overlay com fade/slide próprio (.35s) antes da cascata.
4. Acessibilidade: inert no overlay fechado, Escape fecha, foco
gerenciado (primeiro item ao abrir, botão ao fechar),
lenis.stop()/start().
5. prefers-reduced-motion: sem keyframes (itens visíveis direto).Passo 6 — O padrão em profundidade: acordeão com palavras em cascata (Motion 13)
O FAQ parece simples, mas é onde o padrão "estado da seção dirige palavras" se prova em profundidade. O ícone "+" vira "−" recolhendo a barra vertical; a resposta desdobra em altura via grid-template-rows: 0fr → 1fr (sem medir um pixel); e o refinamento: só depois de aberta, as palavras da resposta sobem em cascata — o Motion 3 disparado pela classe --open do item, com um delay de cortesia de 0.3s.
Crie um componente client "CascadeAccordion" em Next.js:
1. Itens com <button aria-expanded aria-controls> (pergunta em
RevealWords) + ícone:
.icon { position: relative; width: 2rem; height: 2rem; }
.icon-line { position: absolute; top: 50%; left: 50%;
background: currentColor;
transition: transform .4s var(--ease-ui); }
.icon-line--h { width: 100%; height: 1.5px;
transform: translate(-50%, -50%); }
.icon-line--v { height: 100%; width: 1.5px;
transform: translate(-50%, -50%); }
.item--open .icon-line--v { transform: translate(-50%, -50%)
scaleY(0); }
2. Resposta:
.answer-wrap { display: grid; grid-template-rows: 0fr;
transition: grid-template-rows .5s var(--ease-ui); }
.item--open .answer-wrap { grid-template-rows: 1fr; }
.answer-inner { min-height: 0; overflow: hidden; }
3. Cascata pós-abertura: a resposta usa os spans do Motion 3 com:
.item--open .answer .w-inner { transform: translateY(0);
transition-delay: calc(0.3s + var(--d, 0s)); }
(fechado, os inners voltam a translateY(110%) sem delay)
4. Um item aberto por vez (ou multi, por prop); Enter/Espaço nativos
do button; divisórias de 1px entre itens.
5. prefers-reduced-motion: altura sem transition e palavras diretas.Passo 7 — O par de autoplay: depoimentos com anel e citação que se reescreve
Motion 10 — Depoimentos com anel de contagem regressiva
Uma citação por vez, em letras grandes. A seta "próximo" carrega um segredo: um anel se desenha ao redor dela, completando a volta no tempo exato do autoplay — quando o círculo fecha, o slide troca. Pousar o mouse sobre a seta congela o anel (e o autoplay); sair retoma do zero.
O replay do anel é o padrão da casa: o SVG tem key={cycleCount} — cada troca incrementa a chave, o nó remonta, o keyframe recomeça. Simples e à prova de estados fantasma.
Crie um componente client "RingTestimonials" em Next.js:
1. Dados: items[] { quote, author, role, avatar? } + cores por props.
Estados: index, cycleKey (número), paused; INTERVAL = 6000.
2. Autoplay: schedule() = clearTimeout + cycleKey++ +
setTimeout(next, INTERVAL); chame schedule() quando a seção entra
(useHasEntered threshold 0.3) e após cada next()/prev() se
!paused; clique nas setas navega e re-agenda.
3. Anel na seta next:
<button class="arrow arrow--next" onMouseEnter={pause}
onMouseLeave={resume} onFocus={pause} onBlur={resume}>
<svg key={cycleKey} class="ring"
style={{ '--progress-duration': `${INTERVAL}ms` }}
viewBox="0 0 50 50"><circle cx="25" cy="25" r="24"/></svg>
<ArrowIcon/>
</button>
CSS: .ring circle { fill: none; stroke: currentColor;
stroke-width: 1; stroke-dasharray: 151; /* 2πr */
stroke-dashoffset: 151;
animation: ring-fill var(--progress-duration) linear forwards; }
@keyframes ring-fill { to { stroke-dashoffset: 0 } }
.arrow--paused .ring circle { animation-play-state: paused; }
pause(): paused=true + clearTimeout; resume(): paused=false +
schedule() (anel recomeça do zero via nova key).
4. Contador "01 / 05" (padStart) com aria-live="polite"; citação e
autor trocam via Motion 11.
5. prefers-reduced-motion: sem autoplay nem anel (navegação manual).Motion 11 — Citação que se reescreve palavra a palavra
Quando o depoimento troca, a citação nova não desliza de lado: ela se reescreve no mesmo lugar, cada palavra subindo de sua fenda em cascata rápida — incluindo as aspas, que participam como "palavras". É o replay por chave no seu uso mais puro: key={index}, o React remonta, os keyframes rodam do zero. Sem timeline, sem estado de animação. A chave é o play.
Crie um componente "RewriteText" em Next.js:
1. Props: text, replayKey (a chave que força a remontagem), step
(delay entre palavras, default 0.03), baseDelay (0.1).
2. Render:
<p key={replayKey} aria-label={text} class="rewrite">
{splitWords(`“${text}”`).map((w, i) =>
<span class="w" aria-hidden="true">
<span class="w-inner"
style={{ animationDelay:
`${baseDelay + i * step}s` }}>{w}</span></span>)}
</p>
(as aspas tipográficas entram como "palavras" e participam da
cascata, como no site auditado)
3. CSS (keyframes, não transition: precisa rodar a cada remontagem):
.rewrite .w { display: inline-block; overflow: hidden;
padding-bottom: .12em; }
.rewrite .w-inner { display: inline-block;
transform: translateY(100%);
animation: word-in .8s var(--ease-reveal) forwards; }
@keyframes word-in { to { transform: translateY(0) } }
4. Use para citação, autor e contador do Motion 10 (chaves iguais,
baseDelay maior nos secundários).
5. prefers-reduced-motion: animation none, transform none.Passo 8 — A seção mais viva: processo autoplay com cortinas e cor viva (Motion 8)
A seção "como funciona" se apresenta sozinha: a cada 4 segundos avança um passo. Na troca, quatro coisas acontecem em harmonia: a imagem nova sobe cobrindo a anterior como cortina (clip-path de baixo para cima com zoom assentando), os textos velhos saem palavra a palavra por cima enquanto os novos entram por baixo (Motion 3), a linha de progresso preenche no ritmo exato dos 4s, e — o toque de mestre — a cor de fundo e de texto da seção inteira mudam para a paleta daquele passo, vinda do CMS. Clicar num passo assume o controle: o autoplay para de vez e a linha passa a preencher rápido.
Este motion é a tese do site provada: cores no style, transições no CSS, estados dirigindo tudo — e a linha de progresso trocando de transition-duration (4s linear no autoplay, 0.8s com easing no clique) só pela presença da classe --auto.
Crie um componente client "AutoProcess" em Next.js:
1. Dados: steps[] { label, smallLabel, title, description, imageUrl,
bgColor, textColor } (CMS). Estados: active (índice),
entered (useHasEntered threshold 0.2), manual (boolean).
2. Autoplay: ao entered, se !manual: timeout de 4000ms avança active
(para no último); clique num passo seta manual=true, cancela o
timeout e vai direto ao índice. Cleanup do timeout no unmount.
3. Section com style { backgroundColor: stepAtivo.bgColor,
color: stepAtivo.textColor } e
transition: background-color .8s var(--ease-ui), color .8s.
Classes: --entered e --auto (entered && !manual).
4. Lista de passos: cada item com botão (aria-current="step" no
ativo), rótulo em RevealWords e a linha:
.step-line { height: 1px; background:
color-mix(in srgb, currentColor 20%, transparent);
overflow: hidden; }
.step-line-fill { height: 100%; background: currentColor;
transform: scaleX(0); transform-origin: left; }
.step--active .step-line-fill { transform: scaleX(1);
transition: transform .8s var(--ease-reveal); }
.--auto .step--active .step-line-fill {
transition: transform 4s linear; } /* sincronizada ao autoplay */
5. Visual: camadas absolutas empilhadas na ordem dos steps:
.layer { clip-path: inset(100% 0 0 0);
transition: clip-path 1.2s var(--ease-reveal); }
.layer--active, .layer--past { clip-path: inset(0 0 0 0); }
.layer img { transform: scale(1.08); transition: transform 1.2s
var(--ease-reveal); }
.layer--active img { transform: scale(1); }
6. Conteúdo textual por step com estados --active/--past/--future
alimentando o Motion 3 (palavras saem por cima, entram por baixo).
7. Formas decorativas (Motion 15) por step: camada com fade .6s e
replay ao ativar.
8. prefers-reduced-motion: sem autoplay (navegação só por clique) e
trocas por fade simples.Passo 9 — O único pin do site: cards com tombamento 3D (Motion 9)
Um fundo cola na tela e os cards de estatísticas chegam um por cima do outro; enquanto isso, o card que está sendo coberto tomba para trás em perspectiva — inclina, gira um fio e encolhe, como uma carta sendo deitada na mesa pela próxima. Rolar para trás re-ergue as cartas na ordem inversa.
O segredo mecânico: pin: true, pinSpacing: false em todos os cards — todos pinam no mesmo ponto e se acumulam por z-index natural, sem espaçadores. E perspective no pai é obrigatório, ou o rotateX simplesmente não existe visualmente.
Crie um componente client "TiltStack" em Next.js:
1. Estrutura: section.stats > div.stats__bg (100vh, o palco que
pina) + N section.stats__card (cada um 100vh, conteúdo central
com número/frase gigante). CSS: .stats { perspective: 1200px; }
nos wrappers dos cards (necessário para o rotateX aparecer).
2. useGSAP (guarde os triggers num array para cleanup):
const cards = gsap.utils.toArray('.stats__card');
const last = cards[cards.length - 1];
triggers.push(ScrollTrigger.create({ trigger: bgRef.current,
start: 'top top', endTrigger: last, end: 'top top',
pin: true, pinSpacing: false }));
cards.forEach((card, i) => {
triggers.push(ScrollTrigger.create({ trigger: card,
start: 'top top', endTrigger: last, end: 'top top',
pin: true, pinSpacing: false }));
if (i < cards.length - 1) {
const next = cards[i + 1];
triggers.push(ScrollTrigger.create({ trigger: next,
start: 'top bottom', end: 'top top', scrub: 0.5,
onUpdate: (self) => {
const p = self.progress;
gsap.set(card, { rotate: 4 * p, rotateX: 36 * p,
scale: 1 - 0.2 * p });
} }));
}
});
Cleanup: triggers.forEach(t => t.kill()).
Nota: pinSpacing: false é o que faz os cards se ACUMULAREM (sem
espaçadores); a altura total da seção = N viewports naturais.
3. Números dos cards podem usar RevealWords (Motion 3) ao entrar.
4. Mobile: desative (gsap.matchMedia >= 1024px) e caia para uma
lista vertical simples; reduced-motion idem.
5. ScrollTrigger.refresh() após imagens/fonts (pin depende do
layout final).Passo 10 — Interações de conteúdo: abas com saída limpa e a calculadora viva
Motion 12 — Cards em abas com transição de saída/entrada
Ao trocar de aba, os cards atuais se despedem juntos (fade descendo) e o novo conjunto assume, com os textos se reescrevendo. A regra editorial: nunca dois conjuntos visíveis brigando — AnimatePresence mode="wait" garante que sai tudo, depois entra tudo. E reserve a altura do maior conjunto, ou a troca bombeia o layout.
Crie um componente client "TabbedCards" em Next.js:
1. Tabs acessíveis (role=tablist/tab/tabpanel, aria-selected,
setas de teclado). Estado: activeTab.
2. Com Framer Motion (recomendado):
<AnimatePresence mode="wait">
<motion.div key={activeTab} class="cards-grid"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.4,
ease: [0.645, 0.045, 0.355, 1] }}>
{cards.map(…)}
</motion.div>
</AnimatePresence>
(mode="wait" garante: sai tudo, depois entra tudo)
3. Sem Framer: key={activeTab} + keyframe de entrada
(opacity 0/y 10 → 1/0, .4s var(--ease-ui)); a saída vira corte
seco: aceitável, mas registre o downgrade.
4. Títulos dos cards com RevealWords (delay em cascata pelos cards:
--entry-delay = i * .08s).
5. Altura: reserve min-height do maior conjunto (medida no mount)
para a troca não bombear o layout.Motion 14 — Calculadora interativa com resultados vivos
Três sliders, e nenhum botão "calcular": arrastar qualquer um recalcula tudo ao vivo — o número-resultado gigante muda na hora, formatado no padrão local, e cartões de equivalência traduzem o número em comparações palpáveis. O upgrade de motion: amortecer o número com um contador GSAP curto (~0.4s), para o valor "correr" até o novo resultado em vez de trocar seco.
Crie um componente client "LiveCalculator" em Next.js:
1. Props: fórmulas e fatores parametrizáveis (nada hard-coded de
domínio); 3 sliders (input range estilizado com track/thumb
custom) com rótulo e valor ao lado.
2. Cálculo: useMemo derivando resultado e N equivalências dos três
valores; formatação com Number.prototype.toLocaleString(locale).
3. Número vivo: mantenha um ref { v: atual } e, a cada mudança do
resultado, gsap.to(ref, { v: novo, duration: 0.4,
ease: 'power2.out', onUpdate: () => el.textContent =
format(ref.v) }); (contagem curta, não "slot machine")
4. Sliders acessíveis: label + aria-valuetext com unidade; passo e
limites por props; atualização em onInput (não onChange) para o
vivo de verdade.
5. Cartões de equivalência com micro-transition de opacity ao mudar
(classe piscada) e RevealWords na entrada da seção.
6. prefers-reduced-motion: número troca direto, sem contagem.Passo 11 — O fechamento: formas de marca e rodapé parallax
Motion 15 — Formas decorativas de marca por seção
As formas orgânicas da identidade — as curvas gigantes que você vê recortando o hero da Beetogreen — vivem atrás do conteúdo, cada uma com posição, tamanho e rotação próprios. Elas não são estáticas: trocam com fade quando o estado da seção muda (cada passo do processo tem a sua) e reexecutam uma entrada sutil ao voltar à cena — de novo o replay por chave.
Crie um componente "BrandShape" em Next.js:
1. Props: shape (chave num registry de SVGs da marca), position
({ top, left }), size ('60rem'), rotation (graus), color,
centered (boolean), replayKey (opcional).
2. Render: div.shape (position absolute, pointer-events none,
z-index 0, aria-hidden) com style { top, left, width: size,
transform: `${centered ? 'translate(-50%,-50%)' : ''}
rotate(${rotation}deg)` } > SVG (fill/stroke currentColor,
color da prop).
3. Camadas por estado (uso no Motion 8): uma div.shape-layer por
estado, absolute inset-0, opacity 0, transition opacity .6s
var(--ease-reveal); a ativa ganha --active { opacity: 1 } e a
forma interna com key={replayKey} remonta para reexecutar um
keyframe de entrada leve (scale .96 → 1 + fade .6s).
4. Mobile: fator de escala (~0.6) aplicado ao size e recentragem
(top/left 50% + centered), via prop ou matchMedia no pai.
5. As formas são sempre decorativas: nunca recebem foco nem eventos.Motion 16 — Rodapé parallax + voltar ao topo suave
O rodapé chega em parallax de revelação: começa 25% "atrasado" para cima e pousa na posição ao fim da rolagem. O detalhe que evita bugs em produção: clamp() nos dois pontos do trigger — sem ele, páginas mais curtas que o percurso fazem o trigger nascer "já começado" e o parallax pula. E o botão "voltar ao topo" leva a página num voo suave de 1,5s via Lenis, não num teletransporte.
Crie um componente client "ParallaxFooter" em Next.js:
1. Estrutura: div.footer-wrap (ref externa) > footer.footer
(ref interna) com colunas, sociais e créditos.
2. useGSAP (gate: matchMedia sem phone, reduced motion false):
tween = gsap.from(innerRef.current, {
yPercent: -25, ease: 'none',
scrollTrigger: { trigger: wrapRef.current,
start: 'clamp(top bottom)', end: 'clamp(bottom bottom)',
scrub: true } });
Cleanup: tween.scrollTrigger?.kill(); tween.kill();
(clamp() nos dois pontos: essencial para rodapés, evita o trigger
estourar em páginas mais curtas que o percurso)
3. Sociais com keyframe de cascata (mesmo padrão do Motion 7,
social-reveal: opacity 0 + translateY(50%) → visível, delays
por índice) disparado por useHasEntered no rodapé.
4. Botão "voltar ao topo":
const toTop = () => lenis
? lenis.scrollTo(0, { duration: 1.5 })
: window.scrollTo({ top: 0, behavior: 'smooth' });
5. Links com o underline do Motion 4.As 15 armadilhas que vão te morder (leia antes de codar)
"use client"obrigatório em todo componente que toca GSAP, Lenis 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(). - Split de palavras no render, não no browser: quebrar o texto em spans durante o render (util puro) elimina medição de DOM, flash e mismatch de hidratação; reserve SplitText para casos que exijam quebra por LINHA real.
- Replay por chave é um padrão, use-o com intenção: keyframes CSS re-executam quando o nó remonta (
key={n}); é como o site auditado reinicia citações, anéis e formas. Transitions NÃO re-executam assim — escolha keyframe vs transition pelo ciclo de vida. - Timeouts de autoplay têm dono único:
schedule()sempre começa comclearTimeout; pausar cancela; retomar re-agenda do zero; unmount limpa. Dois timers vivos = slides pulando. - Promises de transição resolvem UMA vez: guarde a flag, mate a timeline anterior (
tl?.kill()) antes de criar outra, e resolva também no caminho de erro — a navegação nunca pode ficar presa atrás do overlay. pinSpacing: falseempilha,pinsozinho não: o acúmulo de cards do Motion 9 depende de pinar TODOS no mesmo ponto sem espaçadores; eperspectiveno pai é obrigatório para orotateXexistir visualmente.clamp()em triggers de rodapé: sem ele, páginas curtas fazem o trigger nascer "já começado" e o parallax pula.- Cores dinâmicas do CMS via style inline + transition no CSS: a seção que muda de cor por estado anima porque a
transitionmora no CSS e o valor no style — nunca anime cor via JS por frame. grid-template-rows: 0fr → 1frpara altura animada (acordeão): filho commin-height: 0; overflow: hidden; nunca meça pixels.- Anel SVG:
stroke-dasharray= circunferência real (2πr doviewBox, não do tamanho renderizado); errar esse número quebra o "fecho" do anel com o fim do autoplay. - Máscara de marquee custa GPU:
mask-imageem faixas largas é razoável, mas evite empilhar combackdrop-filterno mesmo eixo de scroll. ScrollTrigger.refresh()após load de imagens e fontes; pins e scrubs posicionais dependem do layout final.prefers-reduced-motionem tudo: sem autoplay, sem anel, sem parallax, sem tombamento; conteúdo direto no estado final — o site auditado desliga na raiz, não "suaviza".
Por onde começar hoje
O trio de entrada: Passo 1 (a infra — useHasEntered e splitWords são meia hora que rendem o site inteiro), Passo 2 (o reveal de palavras — é o sistema nervoso; com ele pronto, cada seção nova já nasce respirando) e Passo 3 (underline e marquee — CSS puro, resultado imediato).
E a série agora soma quatro filosofias: as variáveis contínuas da Plantica, as três camadas da ANAI, os módulos com gates do Studio Modular — e os estados por seção da Beetogreen, com o replay por chave como o truque mais React-idiomático de todos. Quatro sites, nenhuma biblioteca mágica em comum: o que se repete é a disciplina. Estados são a única API — o resto é o CSS fazendo o que sabe fazer.
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 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


