Projeto final: landing page com animacoes intencionais e a11y desde o design
7 min de leitura
Hora de unir tudo. Voce vai construir uma
landing page real (3-5 secoes) com 4
animacoes intencionais - hero fade-in,
theme toggle com View Transitions, card com
layoutId (shared element), parallax de
scroll com useScroll/useTransform - todas
funcionando com e sem prefers-reduced-motion.
E' o ciclo completo: planeja o que animar
(decide o que e essencial vs dispensavel),
implementa, testa em modo reduced, ajusta,
publica.
Esse projeto nao segue o esqueleto de
"explicar conceito + dar exemplo" dos outros
nos. E' um brief de projeto, no estilo de
projects/<slug>.mdx do aprenda-community. Le
ate o fim antes de comecar.
O que voce vai construir
Uma landing page (4-6 secoes) com:
- Hero com fade-in + slide-up - aparece no mount, 1x so.
- Theme toggle (light/dark) com View Transitions - click no botao, cross-fade global de tema.
- Card de feature com
layoutId- 4 cards na grid, click expande pra modal com shared element transition. - Progress bar de scroll -
useScroll+ barra fixa no topo. - Parallax sutil em 1 secao (1-2 camadas).
Stack obrigatoria:
- React 18+ + TypeScript 5+.
- Vite (template
react-ts). - Tailwind ou CSS modules (escolha sua).
motion(ex-Framer Motion).- Sem libs de UI pre-feitas (componentes seus, com excecao de utility headless como Radix se precisar pra a11y de dialog/menu).
prefers-reduced-motionrespeitado em TUDO.
Objetivo
- Consolidar Motion + View Transitions + scroll-linked + a11y num projeto real.
- Praticar a decisao "o que animar" - nao tudo, so o que agrega valor (motion purposeful, nao motion for decoration).
- Ver o ciclo reduced vs normal funcionando
lado a lado: com
prefers-reduced-motion: no-preference, animacoes rodam; comreduce, viram instantaneas ou somem. - Entregar uma landing page que funciona pra 100% dos usuarios, incluindo os 5-15% com sensibilidade a movimento.
Requisitos (minimo)
Setup inicial:
-
pnpm create vite@landing-page -- --template react-ts. -
pnpm add motion. - Estrutura:
src/App.tsx(raiz comMotionConfig).src/components/Hero.tsx(fade-in).src/components/ThemeToggle.tsx(View Transitions).src/components/FeatureGrid.tsx(4 cards).src/components/FeatureModal.tsx(modal comlayoutId).src/components/ScrollProgress.tsx(useScroll).src/components/ParallaxSection.tsx(useTransform).src/hooks/useReducedMotionSafe.ts(helper custom se precisar de logica alem do MotionConfig).
App raiz:
-
<MotionConfig reducedMotion="user">envolvendo TUDO. NAO confiar em cada componente saber. - Estrutura semantica:
<header>,<main>,<footer>,<nav>quando aplicavel. - Lighthouse a11y score >= 95 (sem animacao ativa).
- Sem erros no console (React DevTools + axe DevTools).
Hero (Hero.tsx):
- Fade-in + slide-up no mount (1x so, nao em cada scroll).
- Duracao 0.4-0.6s,
ease: "easeOut". - Em reduced:
y: 0desde o inicio,duration: 0. - Heading principal + subtitulo + CTA.
Theme toggle (ThemeToggle.tsx):
- Botao com
aria-label="Alternar tema". - Click chama
document.startViewTransitionse disponivel; senao, aplica direto. -
view-transition-name: theme-bgno<body>pra cross-fade coordenado. - Persiste escolha em
localStorage. - Em reduced: toggle funciona mas SEM View Transition (aplicacao direta).
Feature grid (FeatureGrid.tsx):
- 4 cards (titulo, descricao, icone SVG).
- Hover: scale 1.02 (ou nada em reduced).
- Click: abre modal.
Feature modal (FeatureModal.tsx):
- Renderizado condicionalmente.
-
AnimatePresenceem volta. - Card clicado tem
layoutId="card-{id}". - Modal tambem tem
layoutId="card-{id}"(mesmo ID). - Click fora ou ESC fecha.
- Focus trap basico (ou use Radix Dialog se preferir).
-
aria-modal="true",role="dialog". - Em reduced: modal aparece/some sem animacao (duration 0).
Scroll progress (ScrollProgress.tsx):
- Barra fixa no topo (4px altura).
-
useScroll()retornascrollYProgress. -
style={{ scaleX: scrollYProgress, transformOrigin: "left" }}. - Em reduced: esconde a barra (nao agrega valor, e' puramente decorativa).
Parallax (ParallaxSection.tsx):
- 1 secao com 2 camadas (foreground + background).
-
useTransformcom ranges diferentes (foreground move menos que background). - Em reduced: camadas em
y: 0, sem parallax.
Decisoes de design (motion purposeful)
Nao anime tudo. A pergunta antes de cada animacao: "se eu remover essa animacao, o usuario ainda entende a UI?" Se sim, ela e' dispensavel e deve ser simplificada em reduced mode. Se nao, ela e' essencial e roda sempre.
Classifique cada animacao do projeto:
- Motion-essential (sempre roda): theme toggle (precisa de feedback visual), modal mount (precisa de "abrir/fechar"), card hover (feedback de "clicavel").
- Motion-simplified (vira simples em reduce): parallax (vira static), scroll progress (some em reduce).
- Motion-disabled (nao roda em reduce): hero fade-in (vira instant), card hover scale (vira sem scale).
Anote essa classificacao no
editorial-decisions.md da trilha.
Desafios extras (stretch goals)
Se voce terminou o minimo e quer ir alem:
- Page transition com Next.js - refazer
o projeto com App Router, usando
<ViewTransition>do Next. - AnimatePresence com mode="popLayout"
- em vez de mode="wait" (default), usar popLayout pra ter o "out" animation e o "in" animation acontecendo simultaneamente com layout shift automatico.
- Loading custom com Rive - integrar um loading spinner feito em Rive, com state machine "loading" / "success" / "error".
- Teste automatizado de a11y com Playwright + axe-core - garantir que cada secao passa axe sem violacoes (e que o modo reduced nao introduz novas).
- Storybook com stories de cada
animacao - cada componente vira uma
story com controles de
prefers-reduced-motioneisDark(integra com trilha storybook-design-systems). - Refactor pra Motion 12 patterns
modernos - usar
useAnimateno lugar de props declarativas em 1-2 casos. -
viewportnowhileInView- ao inves de animar no mount, animar quando a secao entra no viewport (whileInView+viewport={{ once: true }}). - Mascote interativo com Rive -
adicionar 1 mascote com 3 estados (idle,
hover, click), controlado pelos
setBooleanda state machine. - Comparar bundle size com e sem
Motion -
pnpm buildcom e semmotioninstalado, ver o overhead real. - Lighthouse em mobile - testar performance em throttling 3G + CPU 4x slowdown, ajustar se Motion estiver custando LCP/INP.
Dicas
Por onde comecar:
- Setup sem animacao primeiro -
App.tsxcom a estrutura, secoes posicionadas, sem Motion. Garanta que funciona e faz sentido. - MotionConfig na raiz - adicione
<MotionConfig reducedMotion="user">ANTES de qualquer animacao. Isso ja da 80% da a11y gratis. - Hero fade-in - a animacao mais simples, comeca por ela pra aprender o basico.
- Scroll progress - segunda mais simples, linear, sem gestos.
- Theme toggle com View Transitions - requer pensar em fallback de browser.
- Feature grid + modal com
layoutId- o pico de complexidade (shared element entre 2 arvores). - Parallax - ultima, mais sutil.
- Teste em reduced mode - ative reduce no OS, navegue, ajuste o que ficou estranho.
Armadilhas comuns:
- Animar em mount sem
useInView- secoes abaixo da dobra "animam" no load invisivel, desperdicando. UsewhileInViewouuseInViewpra trigger. - View Transitions com 2+ elementos com
mesmo
view-transition-name- browser ignora. Garanta que so UM elemento com cada nome esta visivel por vez. - Esquecer fallback do
startViewTransition- em Firefox 2026 quebra. Sempre faca:
if (document.startViewTransition) {...} else \{ apply(); }.
- em Firefox 2026 quebra. Sempre faca:
prefers-reduced-motionno OS, nao no app - nao adicione "botao de ativar reduced motion". A preferencia vem do OS (que tem 1 unica config que afeta todos os apps). Botao no app confunde.- AnimatePresence sem
keyunica - semkey, exit animation nao roda. Ja vimos isso no no 2. - Drag sem
dragConstraints- elemento arrastado sai da tela. Sempre passe constraints numericas ou ref. - Parallax com 5+ camadas - vira lag perceptivel. 2-3 camadas e' o maximo confortavel.
- Lottie/Rive sem lazy load - bundle
inicial explode. Use
lazy+Suspensepra carregar so quando o componente monta. - Nao testar em mobile - gesture de drag e scroll funcionam diferente. Teste no DevTools mobile emulator + num device real se possivel.
Como validar que terminou:
-
pnpm dev+ abrir no browser + ver todas as animacoes funcionando. - Ativar
prefers-reduced-motionno OS- recarregar + verificar que todas as animacoes estao simplificadas ou removidas.
- Lighthouse a11y score >= 95.
-
pnpm buildsem warning de bundle size absurdo (>500KB gzipped sinal de problema). - Tab pelo site inteiro - foco visivel em tudo, ordem logica.
- Tab no modal - focus trap funciona, ESC fecha.
- axe DevTools sem violacoes critical/serious.
- Testar em Chrome, Firefox (com fallback de View Transitions), e Safari.
- Testar em mobile via DevTools (responsive mode).
Leituras que ajudam durante o projeto:
- Motion - MotionConfig - doc oficial do MotionConfig, com tabela de valores.
- MDN - View Transitions - a referencia completa da API.
- WCAG 2.3.3 - o criterio de "animacao dispensavel", pra referencia.
O projeto final e' onde a trilha vira "sua". As escolhas de o que animar (e o que nao), de quando simplificar em reduced mode, de quando usar View Transitions vs Motion, de quando integrar Rive/Lottie, sao todas suas. O esqueleto dado e' o caminho feliz, desvie quando precisar e anote as decisoes no
editorial-decisions.mdda trilha.