Motions premium em Next.js: os 17 padrões do site da InterGest Canada, passo a passo
GSAP, Framer Motion e Embla no mesmo site sem brigar: os 17 padrões de motion da InterGest Canada em Next.js, com o prompt de implementação pronto de cada um.
- motion
- gsap

Sumário
- A filosofia: cada ferramenta no seu território
- A stack e o papel de cada peça
- O mapa dos 17 motions
- Passo 1 — A fundação: infraestrutura de animação
- Passo 2 — Vitórias rápidas: marquee, botões CTA e banner de cookies
- Passo 3 — O cartão de visitas: hero completo sobre vídeo
- Passo 4 — Produção em série: os quatro reveals de scroll
- Passo 5 — Os trilhos: timeline horizontal e carrossel numerado
- Passo 6 — A mais delicada: seção pinada com troca de steps (Motion 8)
- Passo 7 — A joia da coroa: rotação por frame sequence (Motion 13)
- Passo 8 — O fechamento: formulário, idiomas e o menu overlay
- As 11 armadilhas que vão te morder (leia antes de codar)
- Por onde começar hoje
Quinto estudo de engenharia reversa da série — e depois de quatro arquiteturas autorais (Plantica, ANAI, Studio Modular e Beetogreen), este guia responde uma pergunta diferente: como fica o kit clássico dos sites premium quando bem executado? Qual biblioteca faz o quê, e onde passa a fronteira entre elas?
O auditado é a InterGest Canada (intergestcanada.com) — "Your Gateway to Canadian Expansion": a operação canadense da rede internacional InterGest, que cuida de tudo que uma empresa estrangeira precisa para operar no Canadá, da abertura à gestão diária. O site abre com um voo aéreo sobre Toronto (a CN Tower cortando o título), tipografia branca gigante sobre vídeo, e uma linguagem de motion corporativa e polida — construída em Webflow, o que torna o exercício de tradução para Next.js especialmente instrutivo. Os 17 padrões abaixo são agnósticos de conteúdo: nomes, valores e assets 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 na série, a ordem dos passos é a ordem de construção.
A filosofia: cada ferramenta no seu território
Os quatro guias anteriores tinham teses radicais (CSS-first, três camadas, gates, estados). Este ensina o arranjo mais comum — e a lição está em traçar bem as fronteiras:
- GSAP é a espinha dorsal do espetáculo: reveals, timelines do hero, split text, pin, scrub, contadores, parallax e o frame sequence. Tudo que é coreografia de página passa por ele.
- Framer Motion cuida do ciclo de vida da UI: menu overlay, banner de cookies, estados de formulário, física de mola nos botões. O motivo é um só e vale decorar:
AnimatePresenceresolve animação de desmontagem, que é dolorosa em GSAP puro. Componente que entra E sai → Framer; coreografia de scroll → GSAP. - Embla Carousel para tudo que desliza em trilhos: timeline horizontal e carrossel numerado. Headless, leve, acessível.
- CSS puro onde nenhum JS se justifica: o marquee do rodapé roda no compositor sem uma linha de script.
- Lenis é opcional e declarado como tal: uma escolha de feel, não um requisito dos motions.
A stack e o papel de cada peça
| Biblioteca | Papel no projeto | Por quê |
|---|---|---|
| GSAP 3.13+ e @gsap/react | Espinha dorsal: reveals, timelines, split text, pin/scrub | GSAP é 100% gratuito, incluindo todos os plugins (SplitText, ScrollTrigger, Draggable), desde a aquisição pela Webflow |
| ScrollTrigger (plugin GSAP) | Todas as animações disparadas/controladas por scroll | Pin de seções, scrub, reveals em cascata |
| Lenis | Smooth scroll global (opcional) | Dá o "deslizar amanteigado" típico de sites premium; integra nativamente com ScrollTrigger. É escolha de feel, não requisito: se o projeto pedir scroll nativo, pule o Lenis |
Framer Motion (motion) | Micro-interações de UI: menu overlay, banner de cookies, estados de formulário, hover de botões | AnimatePresence resolve montagem/desmontagem de componentes, que é doloroso em GSAP puro. O pacote atual chama-se motion (Motion for React) |
| Embla Carousel | Timeline horizontal e carrossel de conteúdo | Leve, headless, acessível, com API de dots/setas customizados |
O mapa dos 17 motions
| # | Motion | Uso típico | Gatilho |
|---|---|---|---|
| 1 | Menu overlay fullscreen | Header (hambúrguer) | Clique |
| 2 | Banner de cookies | Global (1ª visita) | Load |
| 3 | Reveal do título hero (split text) | Hero | Load |
| 4 | Entrada sequencial do subtítulo/CTA | Hero | Load |
| 5 | Vídeo de fundo em loop | Hero | Automático |
| 6 | Timeline horizontal navegável | Seção de história/etapas | Setas/arraste |
| 7 | Cards em cascata + hover zoom | Grid de cards com foto | Scroll + hover |
| 8 | Seção pinada com troca de steps | Seção de processo | Scroll |
| 9 | Contadores numéricos animados | Grid de estatísticas | Scroll |
| 10 | Reveal de blocos de texto | Blocos de argumento/vantagem | Scroll |
| 11 | Carrossel com paginação numerada | Seção de serviços/features | Clique/auto |
| 12 | Parallax multicamadas de imagens | Seção editorial com fotos | Scroll |
| 13 | Rotação por frame sequence | Objeto 3D "fake" (globo, produto) | Auto/scroll |
| 14 | Marquee infinito | Rodapé/faixas decorativas | Loop automático |
| 15 | Estados animados de formulário | Newsletter/contato | Envio |
| 16 | Hover de botões CTA | Botões globais | Hover |
| 17 | Seletor de idioma + fade de rota | Header | Hover/clique |
Reveals genéricos de seção (headings e parágrafos que sobem com fade ao entrar no viewport) não têm número próprio: reutilize o padrão dos Motions 7 e 10 (y + autoAlpha com ScrollTrigger once).
Passo 1 — A fundação: infraestrutura de animação
O setup padrão da casa: Lenis sincronizado ao ScrollTrigger com o cleanup em três passos (a causa número 1 de vazamentos quando esquecido), plugins registrados uma única vez, e o hook de prefers-reduced-motion desde o início.
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 embla-carousel-react
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 raf() de uma
// instância destruída (leak)
lenis.off('scroll', ScrollTrigger.update);
lenis.destroy();
- Exponha a instância via React Context com um hook useLenis(), para
outros componentes poderem chamar lenis.stop() / lenis.start()
(o menu overlay do Motion 1 depende disso).
3. Registre os plugins uma única vez num arquivo lib/gsap.ts:
gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP) e exporte gsap.
Esse arquivo só deve ser importado por componentes client.
4. Envolva {children} no layout.tsx raiz com <SmoothScrollProvider>.
5. Crie um hook usePrefersReducedMotion() que lê a media query
(prefers-reduced-motion: reduce); todos os componentes de animação devem
pular ou reduzir animações quando true.
Entregue os arquivos completos e o layout.tsx atualizado.Passo 2 — Vitórias rápidas: marquee, botões CTA e banner de cookies
Três componentes simples que estabelecem o vocabulário — e já demonstram a divisão de territórios: CSS para o marquee, Framer Motion para o que nasce e morre.
Motion 14 — Marquee infinito
Uma faixa de texto que desliza continuamente, como o letreiro de chegadas de um aeroporto. CSS puro, Server Component, zero JS — e a matemática do loop invisível: o track contém dois grupos idênticos e anima translateX(0 → -50%); quando a primeira cópia sai, a segunda está exatamente no lugar dela.
Crie um componente "FooterMarquee" em Next.js (pode ser Server Component —
não precisa de JS):
1. Estrutura:
<div class="overflow-hidden whitespace-nowrap" aria-hidden="true">
<div class="marquee-track inline-flex">
<!-- GRUPO A: a frase repetida K vezes (ex.: 8x, via Array(K).map) -->
<span>SUA FRASE AQUI · </span> ...
<!-- GRUPO B: cópia idêntica do grupo A -->
</div>
</div>
A regra é: o track contém DOIS grupos idênticos, cada um com
exatamente 50% do conteúdo — é isso que faz a matemática do -50%
fechar perfeitamente.
2. CSS (globals.css ou tailwind config):
.marquee-track { animation: marquee 28s linear infinite; }
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
will-change: transform no track.
3. Pause opcional no hover: .marquee:hover .marquee-track
{ animation-play-state: paused; }
4. @media (prefers-reduced-motion: reduce) { .marquee-track
{ animation: none; } }
5. Acessibilidade: aria-hidden no marquee decorativo e uma versão do
texto acessível (sr-only) fora dele.
6. Tipografia grande (text-4xl+), cor sutil (ex. texto vazado com
-webkit-text-stroke ou opacidade baixa), gap consistente entre
repetições.Motion 16 — Micro-interações de hover nos botões CTA
Ao passar o mouse, o botão responde: o fundo sobe deslizando por dentro (um span absoluto revelado no hover), a setinha desloca alguns pixels apontando o caminho, e o clique "afunda" o botão com física de mola. E um detalhe de integração que vale o prompt inteiro: motion.create(Link) — o jeito certo de dar whileTap a um link interno sem perder o prefetch do App Router.
Crie um componente "CtaButton" reutilizável em Next.js:
1. Props: href, children, variant: 'solid' | 'outline', external?: boolean.
2. Base (Tailwind): inline-flex items-center gap-2 rounded-full px-7 py-3.5
relative overflow-hidden group.
3. Efeito "fundo que sobe": <span> absolute inset-0 na cor de hover com
translate-y-full transition-transform duration-500
ease-[cubic-bezier(0.76,0,0.24,1)] group-hover:translate-y-0;
o label fica em <span class="relative z-10"> e troca de cor com
transition-colors sincronizada.
4. Seta: ícone → com transition-transform duration-300
group-hover:translate-x-1.5.
5. Elemento raiz animado:
- Rotas INTERNAS: use next/link com Framer Motion:
const MotionLink = motion.create(Link) // motion(Link) em versões
// anteriores da lib
<MotionLink href={href} whileTap={{ scale: 0.97 }}
transition={{ type:'spring', stiffness: 400, damping: 17 }} />
(preserva prefetch e client-side navigation do App Router)
- Links EXTERNOS: motion.a com target="_blank" rel="noopener".
6. Estados de foco visíveis (focus-visible:ring) — nunca remova outline
sem substituir.Motion 2 — Banner de cookies deslizante
Um segundo e meio depois do site abrir, a caixinha desliza de baixo com mola; aceitar ou rejeitar a faz deslizar de volta — e ela não volta mais. As duas armadilhas de Next.js que o prompt já resolve: localStorage só existe no browser (o componente retorna null até o efeito rodar, evitando hydration mismatch), e o link da política aponta para a rota interna, nunca para staging.
Crie um componente client "CookieBanner" em Next.js:
1. useEffect verifica localStorage.getItem('cookie-consent'); se ausente,
seta visible=true após delay de 1500ms (setTimeout com cleanup).
2. Renderize com <AnimatePresence>: motion.div fixed bottom-6 left-6,
initial { y: 80, opacity: 0 }, animate { y: 0, opacity: 1 },
exit { y: 80, opacity: 0 }, transition spring
{ stiffness: 260, damping: 24 }.
3. Botões "Aceitar" e "Rejeitar": ao clicar, salvam
'accepted'/'rejected' no localStorage e setam visible=false
(o exit anima a saída).
4. Nunca renderize no SSR (retorne null até o efeito rodar) para evitar
hydration mismatch.
5. Inclua link para a rota INTERNA da política de privacidade do próprio
projeto (ex.: /legal/privacy-policy), nunca URL de staging ou domínio
externo.Passo 3 — O cartão de visitas: hero completo sobre vídeo
O hero da InterGest é o trio clássico executado com precisão: título em split text mascarado, elementos secundários em fila, e o voo sobre Toronto rodando atrás de tudo. Atenção à regra que amarra os Motions 3 e 4: o título anima uma única vez, dentro da timeline única — implementar os dois separados faz o título animar em dobro.
Motion 3 — Reveal do título hero (split text mascarado)
Cada palavra do título gigante sobe de dentro de uma máscara invisível, uma ligeiramente depois da outra, com as palavras-chave em destaque. A API moderna do SplitText (3.13+) faz o trabalho pesado: autoSplit espera as fontes carregarem e re-splita se o layout mudar.
Crie um componente client "HeroTitle" em Next.js com GSAP:
1. Importe gsap e SplitText de lib/gsap.ts (plugins já registrados) e o hook
useGSAP de @gsap/react.
2. H1 com ref, contendo o texto com <strong> nas palavras de destaque.
Aplique visibility:hidden inline no H1 para evitar FOUC (flash do texto
sem animação antes do JS rodar).
3. Dentro de useGSAP (scope no container), use a API moderna do SplitText
(3.13+) com autoSplit, que espera as fontes carregarem e re-splita
automaticamente se o layout mudar:
SplitText.create(h1Ref.current, {
type: 'lines,words',
linesClass: 'line',
mask: 'lines', // cria wrappers overflow:hidden automáticos
autoSplit: true, // aguarda document.fonts.ready internamente
onSplit: (self) => {
gsap.set(h1Ref.current, { visibility: 'visible' });
return gsap.from(self.words, {
yPercent: 110, duration: 0.9,
ease: 'power4.out', stagger: 0.06, delay: 0.2
}); // retornar o tween faz o autoSplit
} // gerenciar o cleanup sozinho
});
(Alternativa sem autoSplit: envolva o split em
document.fonts.ready.then(() => { ... }) e chame split.revert()
no cleanup. Nunca faça o split antes das fontes carregarem, senão
as quebras de linha ficam erradas.)
4. Se prefers-reduced-motion, apenas remova o hidden sem animar.Motion 4 — Entrada sequencial do subtítulo e CTA
Depois do título, os elementos secundários entram em fila: slogan, parágrafo, botão — cada um com fade subindo, em overlap negativo ('-=0.4') para a fluidez. O olho é guiado de cima para baixo, na ordem que o designer quer.
Estenda o componente HeroTitle para uma seção "Hero" completa:
1. Estruture: h1 (split do motion 3), p.slogan, p.descricao, a.cta — todos
com classes de seleção dentro do scope do useGSAP.
2. Dentro do onSplit do SplitText (ou após o fonts.ready), crie UMA
gsap.timeline({ defaults: { ease: 'power3.out' } }):
- tl.from(split.words, { yPercent: 110, stagger: 0.06, duration: 0.9 })
- tl.from('.slogan', { y: 30, autoAlpha: 0, duration: 0.6 }, '-=0.45')
- tl.from('.descricao', { y: 30, autoAlpha: 0, duration: 0.6 }, '-=0.4')
- tl.from('.cta', { y: 20, autoAlpha: 0, scale: 0.96, duration: 0.5 },
'-=0.35')
Retorne a timeline no onSplit para o cleanup automático.
3. As sobreposições negativas ('-=') fazem os elementos entrarem em
overlap, dando fluidez (nunca espere um terminar 100% para começar
o próximo).
4. Use autoAlpha (opacity + visibility) em vez de só opacity, para
elementos invisíveis não capturarem cliques.Motion 5 — Vídeo de fundo em loop
Por trás do texto, o voo aéreo roda continuamente — cinematografia, não animação programada. <video> nativo com os quatro atributos mágicos, poster, e a issue conhecida do React com muted resolvida via ref.
No componente Hero do Next.js, adicione o vídeo de fundo:
1. Estrutura: section relative h-svh overflow-hidden
- <video> absolute inset-0 w-full h-full object-cover -z-10 com os
atributos: autoPlay, muted, loop, playsInline,
preload="metadata", poster="/hero-poster.webp"
e <source src="/hero.webm" type="video/webm" /> seguido de
<source src="/hero.mp4" type="video/mp4" /> (webm antes do mp4:
o browser usa a primeira source suportada).
- Overlay: div absolute inset-0 bg-black/40 (legibilidade do texto).
- Atenção ao stacking: -z-10 pressupõe que a section não tenha
background próprio cobrindo o vídeo.
2. IMPORTANTE (React): o atributo muted às vezes não é aplicado no SSR
(issue conhecida do React); garanta via ref em useEffect:
videoRef.current.muted = true e chame .play().catch(() => {})
para cobrir políticas de autoplay.
3. Performance: vídeo comprimido em H.264 (mp4) + AV1/VP9 (webm),
máx ~2-4MB, 1080p, sem faixa de áudio embutida.
4. Acessibilidade: se prefers-reduced-motion, pause o vídeo e mostre
apenas o poster.Passo 4 — Produção em série: os quatro reveals de scroll
Quatro motions, um mesmo padrão (y + autoAlpha disparado por ScrollTrigger) em quatro variações — a fábrica de reveals do site.
Motion 7 — Cards em cascata com hover zoom
Os cards de foto não aparecem juntos: ScrollTrigger.batch() os revela em cascata (~120ms entre cada). Depois, o hover dá zoom lento na foto por dentro da moldura — e isso é CSS puro (group-hover:scale-110), porque hover simples não precisa de JS.
Crie um componente client "FeatureCards" em Next.js:
1. Grid de N cards (grid-cols-1 md:grid-cols-2 xl:grid-cols-4, gap-6).
Card: figure com overflow-hidden rounded-2xl contendo next/image
(fill, object-cover), número editorial (01, 02...), h3 e parágrafo.
2. Reveal com GSAP dentro de useGSAP (scope no container):
ScrollTrigger.batch('.card', {
start: 'top 85%',
once: true,
onEnter: (batch) => gsap.fromTo(batch,
{ y: 60, autoAlpha: 0 },
{ y: 0, autoAlpha: 1, duration: 0.8, ease: 'power3.out',
stagger: 0.12 })
})
Estado inicial via gsap.set('.card', { autoAlpha: 0 }) para não piscar.
3. Hover zoom em CSS: no card use class "group"; na imagem:
transition-transform duration-700 ease-out group-hover:scale-110.
No número: transition-colors group-hover:text-[cor-de-marca].
4. Imagens com next/image, sizes corretos e placeholder blur.
5. once:true garante que a cascata rode uma única vez (não repete ao
rolar para cima e para baixo).Motion 9 — Contadores numéricos animados
Os números grandes sobem contando do zero ao valor final, como velocímetro chegando à marca. Os detalhes que separam o correto do quebrado: prefix/suffix como strings livres (o mundo real tem "+C$" e "590K"), decimals explícito (uma heurística arredondaria 1.46 para "1.5" — dado errado na tela), tabular-nums para o número não tremer de largura, e o valor final renderizado no HTML do servidor — o buscador indexa o dado real, a animação zera e conta só no client.
Crie um componente client "StatsGrid" em Next.js com contadores GSAP:
1. Dados: array de N itens com o tipo:
type Stat = {
label: string; // nome do indicador
value: number; // valor numérico final
prefix?: string; // ex.: 'R$', 'C$', '+', '+C$'
suffix?: string; // ex.: 'B', 'K', 'M', '%'
decimals?: number; // OBRIGATÓRIO quando o valor tem casas
// significativas (ex.: 1.46 → decimals: 2)
description: string;
};
2. Grid responsivo (2 → 5 colunas). Cada card: label, h3 do número
(ref via data-attribute), parágrafo.
3. Contador: dentro de useGSAP, para cada card:
const obj = { val: 0 };
gsap.to(obj, {
val: item.value,
duration: 1.6,
ease: 'power2.out',
scrollTrigger: { trigger: el, start: 'top 85%', once: true },
onUpdate: () => {
el.textContent = (item.prefix ?? '') +
new Intl.NumberFormat('en-CA', {
minimumFractionDigits: item.decimals ?? 0,
maximumFractionDigits: item.decimals ?? 0
}).format(obj.val) +
(item.suffix ?? '');
}
});
(Fixar minimum = maximum evita o número "tremendo" de largura
durante a contagem; considere também font-variant-numeric:
tabular-nums no h3.)
4. Reveal dos cards em cascata com ScrollTrigger.batch (stagger 0.08,
y: 40 → 0, autoAlpha), mesmo padrão do Motion 7.
5. SSR/SEO: renderize o VALOR FINAL formatado no HTML inicial
(server-friendly) e só zere/anime no client após montar — assim o
buscador indexa o número real e usuários sem JS veem o dado.
6. prefers-reduced-motion: pule a contagem e mostre o valor direto.Motion 10 — Reveal de blocos de texto em sequência
Blocos de argumento empilhados, cada um com sua linha divisória que se desenha da esquerda para a direita (scaleX com transform-origin: left) antes do título e do parágrafo entrarem — um trigger individual por bloco, no ritmo do scroll.
Crie um componente client "AdvantageBlocks" em Next.js:
1. N blocos empilhados; cada bloco: hr decorativa no topo, h3 e parágrafo.
2. Em useGSAP, itere gsap.utils.toArray('.advantage'):
para cada bloco crie uma timeline com scrollTrigger
{ trigger: bloco, start: 'top 80%', once: true }:
- Linha: fromTo scaleX 0 → 1, transformOrigin 'left center',
duration 0.8, ease 'power2.inOut'
- h3: from { y: 40, autoAlpha: 0 }, duration 0.7, position '-=0.4'
- p: from { y: 24, autoAlpha: 0 }, duration 0.7, position '-=0.5'
3. Estados iniciais setados via gsap.set para evitar flash.
4. Opcional: transformar em accordion (Radix UI Accordion + animação de
height com grid-template-rows: 0fr → 1fr em CSS) caso o design final
peça blocos expansíveis por clique.Motion 12 — Parallax multicamadas de imagens
Título gigante central e fotos espalhadas que se movem em velocidades diferentes durante o scroll — profundidade de camadas, como a paisagem pela janela do trem. Fator de profundidade declarado por data-speed em cada imagem; ease: 'none' obrigatório (em scrub, a suavização vem do scroll).
Crie um componente client "EditorialParallax" em Next.js:
1. Section relative com min-h-[120vh]; título central enorme em 2 linhas
(com <strong> em palavras de destaque); 2-4 imagens (next/image)
posicionadas absolute em pontos assimétricos (ex.: top-left pequena,
right-center média, bottom-left grande), cada uma com atributo
data-speed: ex. 0.4, 0.8 e 1.4.
2. Em useGSAP, itere gsap.utils.toArray('[data-speed]') e para cada:
gsap.to(el, {
yPercent: -30 * parseFloat(el.dataset.speed),
ease: 'none',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: true
}
});
ease 'none' é obrigatório em scrub (a "suavização" vem do scroll).
3. O título entra com o padrão SplitText do Motion 3, mas disparado por
ScrollTrigger (start 'top 75%', once) em vez de load.
4. Compense o deslocamento: posicione as imagens levemente "adiantadas"
para que fiquem enquadradas quando a seção estiver no centro da tela.
5. Desative o parallax em mobile via gsap.matchMedia('(min-width:768px)')
— em telas pequenas gera custo de performance sem ganho visual.Passo 5 — Os trilhos: timeline horizontal e carrossel numerado
Dois carrosséis Embla com personalidades diferentes.
Motion 6 — Timeline horizontal navegável
A história da empresa corre de lado: marcos com número grande e card, navegados por setas ou arraste, com o item ativo em destaque e uma linha decorativa atravessando as estações.
Crie um componente client "HistoryTimeline" em Next.js com Embla Carousel:
1. Instale embla-carousel-react. Dados: array de N marcos
{ label: string, title: string, description: string }
(label pode ser ano, número de etapa etc.).
2. const [emblaRef, emblaApi] = useEmblaCarousel({ align: 'start',
dragFree: false, skipSnaps: false })
3. Estrutura: div.embla overflow-hidden (ref) > div.embla__container flex >
slides com flex: 0 0 70% (desktop) / 0 0 85% (mobile), gap-8.
Cada slide: label em texto text-7xl font-bold + card com título e texto.
4. Botões Prev/Next chamam emblaApi.scrollPrev()/scrollNext(); desabilite
(opacity-30, cursor-default) nas extremidades usando
emblaApi.canScrollPrev()/canScrollNext() sincronizados via evento
'select' num useEffect (com cleanup do listener).
5. Slide ativo: use selectedScrollSnap() para aplicar classe de destaque
(label em cor de marca, card com scale 1; inativos com opacity-50).
Transições via CSS transition-all duration-500.
6. Linha horizontal decorativa atravessando os marcadores (pseudo-elemento
ou div absoluta) com um ponto em cada estação.
7. Extra: reveal da seção inteira com GSAP ScrollTrigger
(y:60→0, autoAlpha, once: true) quando entra no viewport.Motion 11 — Carrossel com paginação numerada
Serviços num carrossel elegante: imagem-conceito fixa de um lado, painéis de texto trocando do outro, marcadores 01/02/03 que acendem, autoplay opcional — e a micro-entrada do texto a cada troca via key={selectedIndex} num motion.div.
Crie um componente client "NumberedCarousel" em Next.js com Embla:
1. Instale embla-carousel-react e embla-carousel-autoplay.
2. Dados: N itens { index: '01', title, description }.
3. useEmblaCarousel({ loop: true }, [Autoplay({ delay: 6000,
stopOnInteraction: true })]). Autoplay é opcional: exponha via prop.
4. Layout: grid 2 colunas — esquerda: imagem-conceito fixa (next/image)
que NÃO troca; direita: viewport do Embla com os N painéis de texto.
(Variação: se cada item tiver imagem própria, sincronize a troca da
imagem com o evento 'select'.)
5. Navegação inferior: setas prev/next + fileira de marcadores numerados
gerados de scrollSnapList(); clique chama scrollTo(i).
Marcador ativo: cor de marca + underline animado
(after:scale-x-0 → after:scale-x-100 com transition, origin-left);
inativos: opacity-40.
6. Sincronize o índice ativo com o evento 'select' do emblaApi em
useEffect (com cleanup do listener).
7. Micro-motion interna: quando o slide muda, anime o h3 e o parágrafo
do painel ativo com Framer Motion (key={selectedIndex} em um
motion.div com initial { y: 24, opacity: 0 } → animate { y:0,
opacity:1 }, duration 0.5) para o texto "entrar" a cada troca.
8. Acessibilidade: aria-label nas setas, role="tablist"/"tab" nos dots,
pausa do autoplay em hover/focus.Passo 6 — A mais delicada: seção pinada com troca de steps (Motion 8)
Ao chegar na seção de processo, a tela trava: você continua rolando, mas a seção fica presa — a imagem-conceito fixa de um lado enquanto os steps se revezam do outro, no ritmo exato da sua rolagem. Como folhear um livro cuja capa não sai do lugar.
É a técnica canônica de pinned sections (pin: true + scrub + timeline mapeando o progresso), e a mais delicada do catálogo: teste bem a sincronização com o Lenis, use gsap.matchMedia() para o fallback mobile (sem pin), e chame ScrollTrigger.refresh() depois das imagens carregarem.
Crie um componente client "ProcessSection" em Next.js com GSAP ScrollTrigger:
1. Layout: section > div.wrapper (grid 2 colunas; esquerda = imagem-conceito
com next/image; direita = div.steps relative onde os N painéis de step
ficam empilhados com position absolute inset-0).
Opcional: uma coluna/área extra de links relacionados (ex.: lista de
serviços + botão "ver mais"), se o design pedir; ela fica fora da área
animada dos steps.
2. Cada painel: label "Step N", h3 e lista de bullets.
Estado inicial: painel 1 visível; demais com autoAlpha 0 e y 40.
3. Dentro de useGSAP, envolva TUDO em gsap.matchMedia():
const mm = gsap.matchMedia();
mm.add('(min-width: 1024px)', () => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: wrapperRef.current,
start: 'top top',
end: '+=200%', // ~2 viewports de rolagem "presa";
// ajuste proporcional ao nº de steps
pin: true,
scrub: 0.8,
anticipatePin: 1
}
});
tl.to('.step-1', { autoAlpha: 0, y: -40, duration: 1 })
.fromTo('.step-2', { autoAlpha: 0, y: 40 },
{ autoAlpha: 1, y: 0, duration: 1 }, '<0.2')
.to('.step-2', { autoAlpha: 0, y: -40, duration: 1 }, '+=0.5')
.fromTo('.step-3', { autoAlpha: 0, y: 40 },
{ autoAlpha: 1, y: 0, duration: 1 }, '<0.2');
// generalize em loop para N steps
});
mm.add('(max-width: 1023.98px)', () => {
// mobile: sem pin; steps empilhados com reveal simples
// (y + autoAlpha, ScrollTrigger once por step)
});
4. Adicione um indicador de progresso vertical (pontos ou barra) que
acende conforme o progresso — use onUpdate do ScrollTrigger.
5. Na imagem-conceito, um parallax sutil: gsap.to(imagem, { y: -30,
scrollTrigger: { trigger: wrapper, scrub: true } }).
6. Como o projeto pode usar Lenis, garanta que o ScrollTrigger já esteja
sincronizado (Prompt 0). Chame ScrollTrigger.refresh() após carregar
as imagens da seção.Passo 7 — A joia da coroa: rotação por frame sequence (Motion 13)
Na seção final, um objeto parece girar continuamente ao lado da chamada. O truque tem um século: não é 3D — são N fotografias em ângulos progressivos trocando rapidamente, como um flipbook. O mesmo princípio do desenho animado, o mesmo que a Apple usa nas páginas de produto.
Duas decisões definem o resultado: a quantidade de frames (9 frames em ~2s = estética stop-motion proposital; 24–36 por volta = rotação fluida — decida antes de produzir os assets) e o modo (loop automático ou atrelado ao scroll, o "efeito Apple"). E o detalhe fino do loop: animar até N (não N−1) com módulo no draw — é o que elimina o "soquinho" na emenda da volta.
Crie um componente client "FrameSequenceRotator" em Next.js:
1. N imagens em /public/sequence/frame-1.webp ... frame-N.webp
(mesmo tamanho, mesma posição do objeto, ângulos progressivos
cobrindo 360°).
2. Componente com <canvas ref> width/height fixos (ex. 800x800, exibido
responsivo via CSS w-full max-w-md aspect-square).
3. Em useEffect: pré-carregue as N imagens
(new Image(), promise em img.onload; Promise.all antes de animar).
Desenhe o frame 0 assim que carregar.
4. VERSÃO A — rotação automática em loop contínuo:
const frame = { i: 0 };
const total = images.length;
gsap.to(frame, {
i: total, // até N (não N-1) + módulo abaixo:
duration: 2.2, // garante wrap sem "soquinho"
ease: 'none', // no salto do último para o
repeat: -1, // primeiro frame
snap: 'i', // trava em inteiros
onUpdate: () => {
ctx.clearRect(0, 0, w, h);
ctx.drawImage(images[frame.i % total], 0, 0, w, h);
}
});
5. VERSÃO B — controlado por scroll (efeito Apple): mesma tween, mas sem
repeat, i: 0 → total - 1, e com scrollTrigger { trigger: section,
start: 'top bottom', end: 'bottom top', scrub: true } — o objeto gira
conforme a rolagem. Implemente as duas atrás de uma prop
mode: 'auto' | 'scroll'.
6. Só anime quando visível (ScrollTrigger com onToggle pausando/retomando
a tween, ou IntersectionObserver) para economizar CPU.
7. Textos da seção (h2 + CTA) entram com o padrão de reveal dos motions
3-4 via ScrollTrigger.
8. Cleanup: kill na tween e nos observers ao desmontar (o useGSAP com
scope cobre a tween se ela for criada dentro do contexto).Passo 8 — O fechamento: formulário, idiomas e o menu overlay
Motion 15 — Estados animados do formulário
Ao enviar, o formulário desaparece com fade e no lugar surge a mensagem de sucesso — ou o erro em vermelho com um shake sutil (x: [0, -6, 6, -4, 4, 0]). AnimatePresence mode="wait" orquestra a troca; min-height reservado evita o pulo de layout.
Crie um componente client "NewsletterForm" em Next.js:
1. react-hook-form + zod para validar o e-mail; envio via Server Action
(app router) com estados: 'idle' | 'loading' | 'success' | 'error'
em useState.
2. Renderização com <AnimatePresence mode="wait">:
- status !== 'success': motion.form key="form" com input + botão;
exit { opacity: 0, y: -10 }.
- status === 'success': motion.p key="ok" initial { opacity:0, y:10 }
animate { opacity:1, y:0 } — mensagem de sucesso do projeto.
- status === 'error': motion.p key="err" com shake sutil:
animate={{ x: [0, -6, 6, -4, 4, 0] }} transition 0.4s, cor vermelha.
3. Botão em loading: spinner + disabled + leve scale 0.98.
4. Reserve min-height no wrapper para evitar layout shift na troca.
5. Foco/acessibilidade: aria-live="polite" na região das mensagens.Motion 17 — Seletor de idioma + fade de rota
A InterGest atende empresas alemãs expandindo para o Canadá — o toggle "Deutsch" no header não é decoração, é estratégia. Em Next.js: next-intl com routing por prefixo, sublinhado que se desenha no hover, e o fade global de rota via template.tsx.
Implemente o seletor de idioma em Next.js:
1. Configure next-intl com os locales do projeto (ex.: ['pt', 'en']) e
routing por prefixo (/en/...). Crie messages/<locale>.json para cada.
2. Componente "LocaleSwitcher": use os helpers de navegação do next-intl
(createNavigation → { Link, useRouter, usePathname }) para trocar o
locale preservando a rota atual.
3. Estilo do link: relative, com after: absolute bottom-0 left-0 h-px
w-full bg-current scale-x-0 origin-left transition-transform
duration-300 hover:after:scale-x-100 (sublinhado que se desenha).
4. Marque o idioma ativo (aria-current, peso de fonte) e defina o
atributo lang correto no <html>.
5. A transição de página pode ganhar um fade global: envolva o children
do layout num template.tsx com Framer Motion (motion.div initial
{ opacity: 0 } animate { opacity: 1 } duration 0.35) — o template.tsx
remonta a cada navegação, disparando a animação.Motion 1 — Menu overlay fullscreen com stagger
Fechando o ciclo, o menu: uma cortina escura desce cobrindo a tela (clipPath de cima para baixo), e os links surgem um após o outro subindo de máscaras — variants com staggerChildren, o idioma nativo do Framer Motion para cascatas. Com a ergonomia completa: Escape, focus trap, foco devolvido ao hambúrguer, lenis.stop()/start().
Crie um componente Next.js client "FullscreenMenu" com Framer Motion:
1. Um botão hambúrguer no header controla o estado open (useState). O ícone
anima entre hambúrguer e X (duas linhas que rotacionam 45°/-45°).
2. Use <AnimatePresence> para montar/desmontar o overlay:
- Overlay: div fixed inset-0 z-50, fundo escuro; entra com
clipPath: 'inset(0 0 100% 0)' → 'inset(0 0 0% 0)' (efeito cortina
de cima para baixo), duration 0.7, ease [0.76, 0, 0.24, 1];
na saída, reverte.
3. Links de navegação (quantidade parametrizável via array) usam variants:
- container: staggerChildren: 0.07, delayChildren: 0.3
- item: hidden { y: '110%', opacity: 0 } → visible { y: 0, opacity: 1 },
duration 0.6, ease easeOut. Cada link fica dentro de um wrapper com
overflow-hidden para o efeito de "máscara" (texto surgindo de baixo).
4. Coluna lateral opcional com cards de conteúdo: fade + y:20, delay 0.5.
5. Trava de scroll enquanto aberto:
- Com Lenis: chame lenis.stop() ao abrir e lenis.start() ao fechar
(via hook useLenis do Prompt 0). Adicione data-lenis-prevent no
container do overlay se ele tiver conteúdo rolável interno.
- Sem Lenis: document.body.style.overflow = 'hidden' com restore no
cleanup.
6. Feche com tecla Escape e implemente focus trap (foco preso dentro do
overlay enquanto aberto; ao fechar, devolva o foco ao botão hambúrguer).
7. Respeite prefers-reduced-motion: se ativo, use apenas fade rápido.
Tipografia grande (text-5xl+), links com hover que desloca 8px para a direita.As 11 armadilhas que vão te morder (leia antes de codar)
"use client"obrigatório em todo componente que toca GSAP, Lenis ou Framer Motion.- Sempre
useGSAP({ scope })em vez deuseEffectcru — ele fazcontext.revert()automático no unmount, matando tweens e ScrollTriggers órfãos (a causa nº 1 de bugs ao navegar entre rotas do App Router). - Cleanup do Lenis é em 3 passos:
gsap.ticker.remove(update)+lenis.off('scroll', ...)+lenis.destroy(). Esquecer oticker.removedeixa o ticker chamandoraf()de instância morta a cada frame. - Trava de scroll com Lenis: use
lenis.stop()/start(), não apenasoverflow: hiddenno body; edata-lenis-preventem overlays com conteúdo rolável. gsap.matchMedia()para responsividade de animações;ScrollTrigger.matchMediaestá deprecado desde o GSAP 3.11.ScrollTrigger.refresh()após imagens e fontes carregarem — posições de trigger calculadas antes do layout final ficam erradas.- SplitText só depois das fontes: use
autoSplit: true+onSplit(GSAP 3.13+) ou envolva emdocument.fonts.ready.then(). Split antes da fonte = quebras de linha erradas. - Flash de conteúdo (FOUC): estados iniciais de elementos animados devem ser setados via
gsap.set/autoAlphaou classe CSS inicial, nunca só no primeiro frame da animação. - Pin + Lenis: funciona bem desde que a sincronização do Prompt 0 esteja feita; nunca use smooth scroll de duas bibliotecas ao mesmo tempo.
- Anime o título do hero uma única vez: se o hero tiver timeline completa (Motion 4), o tween standalone do Motion 3 não deve existir em paralelo.
prefers-reduced-motionem tudo — além de acessibilidade, evita enjoo em usuários sensíveis (e é critério de auditoria Lighthouse/axe).
Por onde começar hoje
O trio de entrada: Passo 1 (a infra — o cleanup em três passos do Lenis vale o passo sozinho), Passo 2 (marquee, botões e cookies — três vitórias rápidas que já exercitam a divisão de territórios) e Passo 3 (o hero completo — lembrando da regra de ouro: o título anima uma única vez, dentro da timeline).
E com o quinto capítulo, a série ganha seu contraponto: depois das teses autorais da Plantica, da ANAI, do Studio Modular e da Beetogreen, a InterGest mostra o arranjo clássico bem executado — GSAP coreografando a página, Framer Motion administrando o que nasce e morre, Embla nos trilhos e CSS no que roda sozinho. Não é a arquitetura mais ousada da série; é a que você vai mais usar. Dominar as fronteiras entre as ferramentas é o que impede o codebase de virar briga de bibliotecas — e é exatamente isso que este catálogo treina.
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 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
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


