Pular para o conteúdo
Gustavo Tutorial

Referência: Motion

Referência da biblioteca Motion escrita em português, organizada por problema e não por ordem alfabética. Cada função traz o quadro de opções, o exemplo no contexto de página de cliente, e — a parte que documentação nenhuma dá — quando o CSS resolve melhor e você não deveria estar aqui.

Revisão já conversamos sobre isso Novo ainda não vimos 8 partes · 46 tópicos

Nenhum tópico com esse filtro.

PARTE 1

Como o Motion pensa

Antes dos comandos, o modelo. O Motion não é "CSS com JavaScript": ele tem um jeito próprio de tratar tempo, valor e interrupção, e entender isso faz a diferença entre usar a biblioteca e brigar com ela.

De onde ele veio, e por que o nome mudou

Novo

O que hoje se chama Motion nasceu como Framer Motion, a biblioteca de animação do Framer para React. Depois se separou da empresa e virou projeto independente, com uma diferença que importa para você: além da versão React, passou a existir uma versão em JavaScript puro, que funciona em qualquer página — inclusive num HTML único autocontido.

Isso muda a conversa. Antes, usar Motion significava adotar React; hoje, significa uma tag <script>. É o que o traz para dentro do seu tipo de entrega.

Consequência prática na hora de pesquisar: muito material antigo na internet fala de "Framer Motion" e mostra sintaxe de React (<motion.div animate={...}>). Se você está em HTML puro, essa sintaxe não se aplica — procure pela API de JavaScript, que é baseada em funções: animate, scroll, inView, stagger.

As versões — e qual é a sua

Novo
VersãoComo se escrevePara você
JavaScriptFunções: animate(el, {...}, {...})É esta. Funciona em HTML puro, sem build
ReactComponentes: <motion.div animate={...}>Só se um dia o projeto for React
VueComponentes, sintaxe equivalente à do ReactMesma coisa, outro framework

As três compartilham o mesmo motor. As opções de tempo, as curvas e os nomes de mola são idênticos — o que muda é a embalagem. Aprender a versão JavaScript e depois migrar para React custa pouco, porque o vocabulário é o mesmo.

Esta trilha inteira usa a versão JavaScript. A Parte 8 mostra o equivalente em React de cada coisa, para o dia em que fizer sentido — e para você reconhecer o que vê na internet.

Instalar — os três caminhos

NovoJS
1 · global, via CDN
<script src="https://cdn.jsdelivr.net/npm/motion@latest/dist/motion.js"></script>
<script>
  const { animate, scroll, inView, stagger } = Motion;
</script>
2 · módulo ES, sem instalar nada
<script type="module">
  import { animate, stagger } from "https://cdn.jsdelivr.net/npm/motion@latest/+esm";
</script>
3 · com build
npm install motion
# import { animate } from "motion"
CaminhoUse quandoCuidado
Global (CDN)HTML único, entrega de clienteDepende do CDN estar no ar
Módulo ESQuer só uma ou duas funçõesNão roda em file://, precisa de servidor
npmProjeto com Vite, Astro, ReactTraz node_modules junto
Fixe a versão em entrega de cliente. motion@latest é ótimo para estudar e péssimo para um site que vai ficar dois anos no ar: uma mudança grande na biblioteca chega sozinha e quebra a página sem ninguém ter tocado em nada. Troque por uma versão explícita, como motion@13, e atualize quando você quiser.

O que roda por baixo

Novo

O Motion tenta entregar a animação para o próprio navegador executar, usando a API de animações nativa (a mesma do element.animate()). Quando consegue, a animação roda fora da linha principal do JavaScript — ou seja, continua fluida mesmo se o seu código estiver ocupado.

Quando não consegue — porque a propriedade não é acelerável, ou porque é uma mola que depende de cálculo quadro a quadro — ele cai para um motor próprio em JavaScript.

O que você animaCusto
x, y, scale, rotate, opacityBarato — o navegador resolve na placa de vídeo
filter, clipPath, cor de fundoMédio — redesenha, mas não recalcula layout
width, height, top, marginCaro — força recálculo de layout a cada quadro
Esta tabela vale para qualquer técnica de animação, não só para o Motion — é a mesma regra da trilha Movimento. Biblioteca nenhuma torna barato animar width; o que ela faz é tornar conveniente animar a coisa errada.

O que ele faz que o CSS não faz

Novo

A pergunta que decide se vale instalar. São cinco coisas, e só elas:

CapacidadePor que o CSS não dá conta
Mola de verdadeCSS só tem curvas de duração fixa. Mola não tem duração — ela termina quando a energia acaba.
Cascata em lista de tamanho desconhecidonth-child exige você escrever um delay por item. Com 40 itens vindos de dados, não dá.
Interrupção que preserva velocidadeEm CSS, interromper uma transição reinicia do estado atual com velocidade zero. Fica travado.
Sequência longa e controlávelEncadear seis passos com sobreposição em CSS vira uma tabela de delays impossível de manter.
Ler o progresso e usar em outra coisaCSS não te entrega um número de 0 a 1 para você fazer conta.
E o que não está na lista: revelar ao rolar, hover, transição entre páginas, pulso, cascata de três itens fixos. Tudo isso o CSS moderno faz melhor, mais leve e sem dependência. Se o seu projeto só precisa dessas, feche esta trilha e volte para a de Movimento — a decisão certa é não instalar nada.
PARTE 2

animate() — a função central

Noventa por cento do que você vai fazer passa por aqui. Vale entender bem as quatro formas de chamá-la, porque a terceira e a quarta são as que resolvem problema que o CSS não resolve.

As quatro formas de chamar

NovoJS
js
// 1 · um elemento (ou seletor, ou lista de elementos)
animate('.cta', { scale: [0.9, 1] }, { duration: 0.4 });

// 2 · de um estado para outro, explicitamente
animate('.cta', { opacity: [0, 1] });        // array = [de, para]
animate('.cta', { opacity: 1 });             // sem array = do atual até 1

// 3 · um número solto, sem elemento nenhum
animate(0, 248, {
  duration: 1.4,
  onUpdate: v => contador.textContent = Math.round(v)
});

// 4 · uma sequência de passos (ver Parte 4)
animate([
  ['.titulo', { y: [20, 0], opacity: [0, 1] }, { duration: 0.5 }],
  ['.cta',    { scale: [0.9, 1] },             { at: '-0.2' }]
]);
A terceira forma é a mais subestimada. Ela anima um número, não um elemento — e te entrega o valor a cada quadro no onUpdate. É com ela que se faz contador, barra de progresso com texto, contagem de vagas, valor de orçamento subindo. Nada disso tem a ver com CSS.

O que dá para animar

NovoJS

A diferença mais prática em relação ao CSS: cada transformação é uma propriedade independente. Em CSS, transform é uma string só — animar x e rotate em tempos diferentes exige ginástica. Aqui não.

js
animate('.selo', {
  x: 40,            // translateX
  y: [0, -12, 0],   // keyframes: sobe e volta
  rotate: 8,
  scale: 1.1,
  opacity: 0.9,
  backgroundColor: '#305546',
  borderRadius: '12px',
  filter: 'blur(2px)',
  '--brilho': 1     // variável CSS também anima
}, { duration: 0.6 });
GrupoNomes
Posiçãox, y, z
Escalascale, scaleX, scaleY
Rotaçãorotate, rotateX, rotateY, rotateZ
Inclinaçãoskew, skewX, skewY
Outrosopacity, qualquer propriedade CSS pelo nome, variáveis CSS
Unidade omitida em x e y significa pixels. Porcentagem também vale, e é relativa ao próprio elemento: x: '100%' move o elemento pela própria largura — ótimo para carrossel e menu deslizante.

As opções de tempo

NovoJS
OpçãoPadrãoO que faz
duration0.3 (tween)Segundos, não milissegundos — diferente do CSS
delay0Espera antes de começar. Aceita stagger().
repeat0Quantas vezes repetir. Infinity para sempre.
repeatType"loop"loop recomeça, reverse volta, mirror volta com a curva espelhada
repeatDelay0Pausa entre repetições
times—Onde cada keyframe cai, de 0 a 1
ease"easeOut"A curva — ver Parte 3
js
// times: por padrão os keyframes são igualmente espaçados.
// aqui o pico acontece em 30% do tempo, e os 70% restantes são a descida:
animate('.selo', { scale: [1, 1.25, 1] }, {
  duration: 0.8,
  times: [0, 0.3, 1],
  ease: 'easeOut'
});
Segundos, não milissegundos. É o erro número um de quem vem do CSS: escrever duration: 300 achando que são 300ms. São cinco minutos. Se a animação "não está acontecendo", confira isso antes de qualquer outra coisa.

Tempo diferente para cada propriedade

NovoJS

Recurso que o CSS não tem e que é o que dá "acabamento" à animação: dentro do objeto de opções, cada propriedade pode ter o próprio tempo e a própria curva.

js
animate('.card-servico',
  { opacity: [0, 1], y: [24, 0], scale: [0.96, 1] },
  {
    duration: 0.5,                              // padrão para todas
    opacity: { duration: 0.25 },                // a opacidade entra mais rápido
    y:       { type: 'spring', bounce: 0.2 },   // o deslocamento tem mola
    scale:   { duration: 0.6, ease: 'backOut' } // e a escala passa um pouco
  }
);
Por que isso importa: opacidade e movimento devem terminar em momentos diferentes. Quando os dois acabam juntos, o olho percebe o elemento "chegando e parando"; quando a opacidade termina antes, o elemento parece já estar lá e só se acomodando. É uma diferença de 200 milissegundos que separa animação amadora de profissional.

O objeto de controle

NovoJS

animate() devolve um objeto com o qual você comanda a animação depois de iniciada.

js
const a = animate('.faixa', { x: '-50%' }, { duration: 14, repeat: Infinity, ease: 'linear' });

a.pause();       // congela
a.play();        // segue
a.cancel();      // desfaz e volta ao estado original
a.complete();    // pula direto para o fim
a.stop();        // para onde está, sem desfazer

a.time  = 2;     // pula para o segundo 2 (também lê)
a.speed = 0.5;   // câmera lenta; negativo roda de trás para frente
a.duration;      // duração final, já calculada (útil com mola)

await a;         // dá para esperar o fim — o objeto é "thenable"
a.then(() => console.log('acabou'));
uso real: pausar a faixa no hover
const faixa = animate('.marquee__fita', { x: '-50%' },
                      { duration: 14, repeat: Infinity, ease: 'linear' });

const area = document.querySelector('.marquee');
area.addEventListener('pointerenter', () => faixa.pause());
area.addEventListener('pointerleave', () => faixa.play());
a.duration é especialmente útil com mola: como a mola não tem duração declarada, esse é o jeito de descobrir quanto ela realmente vai levar — por exemplo, para disparar a próxima coisa no momento certo.

Interromper sem quebrar

NovoJS

Este é o motivo técnico mais forte para usar uma biblioteca de animação em interface que responde a gesto. Quando você chama animate() de novo no mesmo elemento, a animação anterior é substituída preservando a velocidade atual — o elemento continua o movimento em vez de dar um solavanco.

js
const menu = document.querySelector('.menu');

function abrir()  { animate(menu, { x: 0 },      { type: 'spring', bounce: 0.15 }); }
function fechar() { animate(menu, { x: '-100%' }, { type: 'spring', bounce: 0.15 }); }

// abrir e fechar rápido, várias vezes: o menu muda de direção
// sem parar no meio do caminho. Em CSS, cada troca reinicia do zero.
Em CSS isso não existe: interromper uma transition faz o elemento recomeçar do ponto atual com velocidade zero. Em movimento rápido, o olho vê a parada. É invisível numa página estática e muito visível num menu que o usuário abre e fecha com o polegar.

Respeitar quem pediu menos movimento

NovoJS

Biblioteca de animação não lê prefers-reduced-motion sozinha. Em CSS você resolve com uma media query; aqui, é sua responsabilidade — e é obrigação, não capricho.

js
const calmo = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// versão 1: não anima, só aplica o estado final
if (calmo) {
  document.querySelectorAll('.revela').forEach(el => el.style.opacity = 1);
} else {
  animate('.revela', { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.06) });
}

// versão 2: anima, mas sem deslocamento e bem mais curto
animate('.revela',
  calmo ? { opacity: [0, 1] } : { opacity: [0, 1], y: [20, 0] },
  { duration: calmo ? 0.01 : 0.5 }
);
A versão 2 costuma ser melhor: quem ativou a preferência não quer nenhum movimento, mas ainda precisa perceber que algo mudou. Opacidade curta cumpre isso sem provocar enjoo — que é o sintoma real de quem liga essa opção.
PARTE 3

Curvas: tween, spring e inertia

A curva é o que faz a animação parecer barata ou cara — mais do que a duração, mais do que a distância. O Motion tem três motores, e escolher o certo é metade do resultado.

tween — duração fixa, curva declarada

NovoJS

É o motor padrão e o mais próximo do que você já conhece do CSS: você diz quanto tempo dura e qual o formato da aceleração.

js
animate('.card', { y: [20, 0] }, { type: 'tween', duration: 0.5, ease: 'easeOut' });
// type: 'tween' é o padrão — pode omitir

As curvas com nome, e onde cada uma serve:

NomeSensaçãoUse em
linearMecânica, sem aceleraçãoRotação contínua, faixa infinita, barra ligada à rolagem
easeOutEntra rápido e desaceleraO padrão para quase tudo que aparece. É como as coisas chegam no mundo real
easeInComeça devagar e aceleraSó para o que sai da tela
easeInOutSuave nas duas pontasMovimento longo, de um ponto a outro
backOutPassa do alvo e voltaBotão, selo, ícone — dá vida sem custar peso
backInRecua antes de irSaída com intenção
circOutDesaceleração bem acentuadaQuando easeOut parece mole demais
anticipateRecua, depois disparaUm elemento só, uma vez. Cansa rápido.
Cada uma dessas tem as variantes In, Out e InOut. Na dúvida, easeOut: é o que o olho lê como natural, porque objeto que chega no mundo físico desacelera até parar.

Curva própria — bezier e steps

NovoJS
js
// os quatro números do cubic-bezier, iguais aos do CSS
animate('.card', { y: [20, 0] }, { ease: [0.22, 1, 0.36, 1] });

// curva por keyframe: uma para cada trecho
animate('.selo', { scale: [1, 1.2, 1] }, { ease: ['easeOut', 'easeIn'] });

// movimento em degraus, sem suavização
animate('.digito', { y: -100 }, { ease: steps(5) });

// e a sua própria função: recebe 0→1, devolve 0→1
animate('.x', { x: 100 }, { ease: t => t * t });
A curva que eu usaria como padrão do seu design system: [0.22, 1, 0.36, 1]. Ela sai forte e freia bastante no fim — é o que dá a sensação de "caro" sem exagero. Declare uma vez como token e use no site inteiro; movimento coerente vale mais que movimento variado.

spring — o jeito novo, com dois números

NovoJS

Mola não tem duração: ela termina quando a energia acaba. Isso sempre foi difícil de ajustar, porque os parâmetros físicos não têm relação intuitiva com o resultado. A forma moderna resolve com dois números que você entende de cara:

js
animate('.cta', { scale: [0.9, 1] }, {
  type: 'spring',
  bounce: 0.25,          // 0 = sem quique; 1 = muito quique
  visualDuration: 0.4    // quanto tempo até "parecer" que chegou
});
bounceResultadoOnde usar
0Chega e para, sem passar do pontoMenu, gaveta, painel
0.1 – 0.2Um toque de vida, quase imperceptívelCard, seção entrando
0.25 – 0.4Quique claro e simpáticoBotão, selo, ícone
0.5+Brincalhão, chama atençãoConfete, mascote. Em site de serviço, raramente.
visualDuration não é a duração total. É o tempo até o elemento parecer ter chegado ao destino — o quique residual continua depois. É por isso que ele é útil: você ajusta pela percepção, não pelo relógio.

spring — o jeito físico, com três números

NovoJS

O modelo clássico, que você vai encontrar em todo material antigo e em outras bibliotecas. Continua funcionando e dá controle mais fino.

js
animate('.cta', { scale: [0.9, 1] }, {
  type: 'spring',
  stiffness: 260,   // força da mola: maior = mais rápido e agressivo
  damping: 20,      // freio: menor = mais quique; maior = para antes
  mass: 1,          // peso: maior = mais lento e pesado
  velocity: 0,      // velocidade inicial
  restDelta: 0.01   // o quão perto do alvo conta como "chegou"
});
CombinaçãoSensação
stiffness: 120, damping: 20Lenta e suave — gaveta grande
stiffness: 260, damping: 20Equilibrada — serve para quase tudo
stiffness: 400, damping: 30Rápida e firme — resposta a toque
stiffness: 260, damping: 10Quicando bastante
stiffness: 700, damping: 40Quase instantânea, com um resquício de física
Qual dos dois usar: comece por bounce e visualDuration — você acerta em duas tentativas. Vá para o trio físico só quando precisar de algo específico que os dois números não alcançam, ou quando estiver traduzindo um valor de outra biblioteca.

inertia — quando o dedo solta

NovoJS

O terceiro motor, e o mais específico: ele pega a velocidade que o elemento já tinha e a desacelera, como um objeto deslizando. É o que existe por trás da rolagem com inércia do celular.

js
animate('.carrossel', { x: posicaoFinal }, {
  type: 'inertia',
  velocity: velocidadeDoGesto,   // vem do evento de ponteiro
  power: 0.8,                    // quanto a velocidade projeta distância
  timeConstant: 700,             // quão rápido desacelera
  min: -1200, max: 0,            // limites — encosta e para
  bounceStiffness: 500           // e o que acontece ao bater no limite
});
Na prática, você quase não chama isso direto. Ele aparece quando você usa o recurso de arrastar — em React, é o que o drag usa ao soltar. Para site de serviço local, é o motor que você mais vai ler sobre e menos vai usar. Está aqui para o mapa ficar completo.

Qual motor para qual situação

Novo
SituaçãoMotorAjuste inicial
Elemento entrando na telatweenduration: 0.5, ease: 'easeOut'
Lista em cascatatweenduration: 0.45 + stagger(0.06)
Botão respondendo a cliquespringbounce: 0.3, visualDuration: 0.3
Menu ou gaveta abrindospringbounce: 0, visualDuration: 0.35
Elemento seguindo o ponteirospringstiffness: 400, damping: 30
Faixa infinitatweenease: 'linear', repeat: Infinity
Ligado à rolagemtweenease: 'linear' — a rolagem já é a curva
Depois de arrastarinertiaCom a velocidade do gesto
Duas regras que cobrem o resto: mola para o que responde a gesto humano, tween para o que acontece sozinho. E linear nunca em entrada — movimento sem aceleração parece máquina, e é o sinal mais rápido de animação malfeita.
PARTE 4

stagger e sequências

Duas ferramentas para coordenar várias coisas no tempo. A primeira resolve "a mesma animação, defasada"; a segunda resolve "coisas diferentes, em ordem". São os dois casos em que o CSS trava de verdade.

stagger — a cascata

NovoJS

stagger() não é uma animação: é um gerador de delay. Você o passa no lugar do delay, e ele calcula um valor diferente para cada elemento da lista.

js
const { animate, stagger } = Motion;

animate('.servico',
  { opacity: [0, 1], y: [20, 0] },
  { duration: 0.45, delay: stagger(0.06) }
);
// item 1 em 0s, item 2 em 0.06s, item 3 em 0.12s… e assim por diante,
// independente de serem 3 ou 300 itens
OpçãoPadrãoO que faz
1º argumento—Segundos entre um item e o seguinte
startDelay0Espera antes do primeiro item
from"first"De onde a onda parte: "first", "center", "last", ou um índice
ease"linear"Curva da distribuição dos delays, não da animação
js
// a onda sai do meio da grade para as bordas
animate('.foto', { opacity: [0, 1], scale: [0.92, 1] }, {
  duration: 0.5,
  delay: stagger(0.05, { from: 'center', startDelay: 0.2 })
});

// delays que vão ficando mais juntos: começa espaçado, termina rápido
animate('.linha', { x: [-20, 0], opacity: [0, 1] }, {
  delay: stagger(0.08, { ease: 'easeOut' })
});
O número certo: entre 0.04 e 0.08 segundos. Acima de 0.1, com dez itens, a última coisa aparece um segundo depois da primeira — e o visitante já rolou a página. Cascata existe para dar ritmo, não para atrasar leitura: se a lista tem mais de doze itens, reduza para 0.03 ou anime só os primeiros.

Quando o CSS resolve o mesmo

RevisãoCSS

Honestidade antes da ferramenta: se a lista tem tamanho fixo e você escreve o HTML, o CSS faz a mesma cascata sem uma linha de JavaScript.

css
<div class="servico" style="--i:0">…</div>
<div class="servico" style="--i:1">…</div>
<div class="servico" style="--i:2">…</div>

.servico {
  opacity: 0; transform: translateY(20px);
  animation: sobe .45s cubic-bezier(.22,1,.36,1) forwards;
  animation-delay: calc(var(--i) * 60ms);
}
Use CSS quandoUse stagger quando
A lista é fixa e você escreve o HTMLOs itens vêm de dados e você não sabe quantos são
Três, quatro, cinco itensA quantidade muda com filtro ou busca
Não quer dependênciaPrecisa de from: 'center' ou ordem que não seja a do DOM
Vai animar uma vez, ao aparecerPrecisa reanimar, inverter ou interromper
Nas suas landing pages, quase sempre a coluna da esquerda vence. A coluna da direita aparece em catálogo, listagem filtrável e galeria — que é exatamente quando o projeto já deixou de ser uma landing page.

Sequência — coisas diferentes, em ordem

NovoJS

Quando os elementos são diferentes e cada um tem a própria animação, você passa um array de passos. Cada passo é [alvo, propriedades, opções].

js
animate([
  ['.hero h1',    { y: [24, 0], opacity: [0, 1] }, { duration: 0.5 }],
  ['.hero p',     { y: [16, 0], opacity: [0, 1] }, { duration: 0.45, at: '-0.25' }],
  ['.hero .cta',  { scale: [0.9, 1], opacity: [0, 1] }, { type: 'spring', bounce: 0.3, at: '-0.2' }],
  ['.hero .selo', { opacity: [0, 1] }, { duration: 0.3, at: '<' }]
]);
atQuando o passo começa
omitidoDepois que o anterior terminar
'-0.25'0,25s antes do anterior terminar — sobreposição
'+0.3'0,3s depois do anterior terminar — respiro
'<'Junto com o passo anterior
1.2No segundo 1,2 da sequência, em tempo absoluto
A sobreposição é o segredo da hero. Sem at, cada elemento espera o anterior acabar e a entrada leva dois segundos — parece lenta. Com -0.2 entre eles, o conjunto entra em menos de um segundo e ainda se lê como uma sequência, não como um bloco.

Controlar a sequência inteira

NovoJS

A sequência devolve o mesmo objeto de controle de uma animação simples — e ele comanda o conjunto todo.

js
const entrada = animate([
  ['.hero h1',   { y: [24, 0], opacity: [0, 1] }],
  ['.hero .cta', { scale: [0.9, 1] }, { at: '-0.2' }]
]);

entrada.speed = 0.25;        // câmera lenta, para você ajustar olhando
entrada.time = 0.4;          // pula para um instante específico
await entrada;               // espera tudo terminar
entrada.cancel();            // desfaz o conjunto
Truque de ajuste: enquanto está calibrando uma sequência, rode com speed = 0.2. Em velocidade normal é impossível enxergar se a sobreposição está boa; a quatro vezes mais lento, os erros de tempo ficam óbvios. Depois volte para 1 e confira.

Sequência do Motion × timeline do GSAP

Novo
Motion — sequênciaGSAP — timeline
FormatoUm array, declarado de uma vezObjeto encadeado, construído passo a passo
Sobreposiçãoat: '-0.2''-=0.2'
Simultâneoat: '<''<'
Montar em partesDifícil — o array é fechadoNatural: guarda a timeline e vai somando
Rótulos nomeadosNão temTem — addLabel e salto por nome
Aninhar sequênciasNãoTimeline dentro de timeline
PesoBem menorMaior, e cresce com plugin
A leitura prática: para uma hero de quatro elementos, a sequência do Motion é mais limpa e pesa menos. Para uma narrativa longa de rolagem, com dez momentos que você monta em partes e quer poder pular por nome, a timeline do GSAP não tem concorrente — e é aí que instalar GSAP se paga.
PARTE 5

scroll() — ligar movimento à rolagem

A função que dá acesso ao progresso da rolagem como um número de 0 a 1. É o recurso mais poderoso da biblioteca e o mais fácil de exagerar — vale ler a última parte deste bloco antes de sair usando.

As duas formas

NovoJS
js
const { scroll, animate } = Motion;

// 1 · função de retorno: você recebe o progresso e faz o que quiser
scroll(progresso => {
  barra.style.transform = `scaleX(${progresso})`;
});

// 2 · animação amarrada à rolagem: em vez de rodar no tempo,
//     ela avança conforme a página desce
scroll(
  animate('.barra', { scaleX: [0, 1] }, { ease: 'linear' })
);

// as duas devolvem uma função para desligar
const parar = scroll(p => { /* … */ });
parar();
Use ease: 'linear' em animação amarrada à rolagem. A curva já é o gesto da pessoa: se você acrescentar uma curva por cima, o movimento fica descompassado do dedo dela, e a sensação é de atraso.

As opções

NovoJS
OpçãoPadrãoO que faz
containerwindowQual área rola. Aponte para um elemento se o scroll for interno.
axis"y""x" para carrossel horizontal
targettoda a áreaMedir a passagem de um elemento, não da página inteira
offset["start start", "end end"]Onde o progresso começa e termina
trackContentSizefalseRemede se o conteúdo mudar de altura
js
scroll(
  animate('.depoimento', { opacity: [0, 1, 1, 0] }, { ease: 'linear' }),
  {
    target: document.querySelector('.secao-depoimentos'),
    offset: ['start end', 'end start']
  }
);
trackContentSize resolve um bug silencioso: se a página tem imagem sem width/height, ou conteúdo que abre, a altura muda depois do cálculo e o progresso fica errado. Ligue quando houver acordeão, galeria ou conteúdo carregado depois.

offset — a gramática das duas palavras

NovoJS

A parte que confunde todo mundo na primeira vez. Cada entrada do array tem duas palavras: a primeira é um ponto do elemento, a segunda é um ponto da tela. O progresso vale 0 quando esses dois pontos se encontram.

js
offset: ['start end', 'end start']
//         │      │      │     └── topo da tela
//         │      │      └── fim do elemento
//         │      └── base da tela
//         └── começo do elemento

// leitura: "vale 0 quando o topo do elemento encosta na base da tela;
//           vale 1 quando o fim do elemento sai pelo topo da tela"
offsetCobreUse para
['start end', 'end start']Toda a travessia pela telaParallax, fade de entrada e saída
['start end', 'start start']Da entrada até encostar no topoRevelar enquanto sobe
['start start', 'end end']Enquanto o elemento é maior que a telaProgresso de leitura de um artigo
['center end', 'center start']Centrado no elementoEfeito que tem pico no meio

Além das palavras, valem número de 0 a 1, pixels e unidades de tela: offset: ['start 80vh', 'end 20vh'].

Como acertar sem adivinhar: imprima o progresso no console enquanto rola — scroll(p => console.log(p.toFixed(2)), { target, offset }). Em dez segundos você vê se o 0 e o 1 caem onde você queria. É mais rápido que raciocinar sobre as palavras.

O segundo argumento: velocidade e distância

NovoJS

A função de retorno recebe um segundo parâmetro com dados crus da rolagem — inclusive a velocidade, que abre efeitos que o progresso sozinho não dá.

js
scroll((progresso, info) => {
  info.y.current;       // pixels já rolados
  info.y.progress;      // o mesmo 0→1 do primeiro argumento
  info.y.velocity;      // pixels por segundo; negativo = subindo
  info.y.scrollLength;  // quanto há para rolar no total
});
uso real: esconder o cabeçalho ao descer, mostrar ao subir
const topo = document.querySelector('.topo');
let escondido = false;

scroll((_, info) => {
  const descendo = info.y.velocity > 20;
  const subindo  = info.y.velocity < -20;
  const passouDaDobra = info.y.current > 200;

  if (descendo && passouDaDobra && !escondido) {
    escondido = true;
    animate(topo, { y: '-100%' }, { duration: 0.25, ease: 'easeIn' });
  } else if (subindo && escondido) {
    escondido = false;
    animate(topo, { y: '0%' }, { type: 'spring', bounce: 0 });
  }
});
Repare no controle escondido: sem ele, você dispararia uma animação nova a cada quadro de rolagem. É o erro de desempenho mais comum em efeito ligado a scroll — e o sintoma é o celular esquentando.

Três receitas prontas

NovoJS
1 · barra de progresso de leitura
scroll(animate('.barra-topo', { scaleX: [0, 1] }, { ease: 'linear' }));
// .barra-topo { position:fixed; top:0; left:0; right:0; height:3px;
//               background:var(--accent); transform-origin:0 50%; }
2 · parallax discreto na foto da hero
scroll(
  animate('.hero__foto', { y: [0, -60] }, { ease: 'linear' }),
  { target: document.querySelector('.hero'), offset: ['start start', 'end start'] }
);
// 60px é o teto. Acima disso a foto descola do texto e parece defeito.
3 · contador que conta conforme a seção passa
const alvo = 248, el = document.querySelector('.numero');
scroll(p => { el.textContent = Math.round(alvo * p); }, {
  target: document.querySelector('.secao-numeros'),
  offset: ['start end', 'center center']
});

O custo que ninguém menciona

Novo

Efeito ligado à rolagem é o que mais impressiona em portfólio e o que mais atrapalha em página de venda. Quatro motivos concretos:

ProblemaPor quê
Atrasa a leituraO visitante rola para ler, não para assistir. Conteúdo que só aparece depois do efeito é conteúdo escondido.
Quebra a busca dentro da páginaCtrl+F encontra texto com opacity: 0 e leva a pessoa a uma área aparentemente vazia
Enjoa de verdadeParallax é a principal queixa de quem tem sensibilidade vestibular
Custa em celular fracoCada quadro de rolagem roda seu código, e a maioria do tráfego local é Android de entrada
O limite que eu recomendo para landing page de serviço: um efeito de rolagem por página, no máximo — geralmente a barra de progresso ou um parallax de 40 a 60 pixels na hero. Tudo além disso, desligue no celular e sob prefers-reduced-motion. A página tem que vender, não impressionar outro desenvolvedor.
PARTE 6

inView e gestos

As funções que reagem ao visitante: quando um elemento entra na tela, quando o ponteiro passa, quando o dedo pressiona. Todas devolvem uma função de limpeza — e usá-la é o que separa código que funciona de código que vaza memória.

inView — entrou na tela

NovoJS
js
const { inView, animate, stagger } = Motion;

inView('.secao', (elemento) => {
  animate(elemento.querySelectorAll('.card'),
          { opacity: [0, 1], y: [20, 0] },
          { duration: 0.45, delay: stagger(0.06) });
});
OpçãoPadrãoO que faz
roota janelaAponte para um elemento se a rolagem for interna
margin"0"Aumenta ou reduz a área de disparo. "0px 0px -15% 0px" dispara um pouco antes de entrar de fato.
amount"some""some", "all", ou de 0 a 1 — quanto do elemento precisa aparecer

E o detalhe mais útil: se a sua função devolver outra função, ela é chamada quando o elemento sai da tela.

js
const parar = inView('.video-secao', (el) => {
  const v = el.querySelector('video');
  v.play();
  return () => v.pause();     // roda ao sair da tela
}, { amount: 0.5 });

parar();   // desliga o observador de vez
Anime uma vez, não toda vez. Por padrão o retorno de saída faz o efeito repetir a cada passagem, e conteúdo que pisca a cada rolagem é irritante. Se for entrada de seção, marque o elemento e não reanime — ou use o CSS, que com animation-timeline: view() resolve isso sem JavaScript nenhum.

hover e press

NovoJS

À primeira vista parecem só um atalho para addEventListener. A diferença está nos casos de borda que eles já resolvem.

js
const { hover, press, animate } = Motion;

hover('.card', (elemento) => {
  animate(elemento, { scale: 1.03 }, { type: 'spring', bounce: 0.2 });
  return () => animate(elemento, { scale: 1 }, { type: 'spring', bounce: 0.2 });
});

press('.cta', (elemento) => {
  animate(elemento, { scale: 0.96 }, { duration: 0.1 });
  return () => animate(elemento, { scale: 1 }, { type: 'spring', bounce: 0.4 });
});
O que eles resolvem sozinhos
hover ignora toque — no celular, um toque não dispara "hover grudado"
press encerra corretamente se o dedo sair do elemento antes de soltar
Os dois aceitam seletor e valem para elementos que aparecem depois
Os dois devolvem função de limpeza, e a de "saída" vem do seu retorno
Ainda assim, pense duas vezes. Hover e press em CSS são uma linha, funcionam sem JavaScript e nunca falham. Só vale trazer para cá quando a resposta precisa de mola ou precisa interromper outra animação em curso — que é exatamente o que o CSS não faz. Para um card que levanta 4 pixels, continue no CSS.

Limpeza — o que ninguém ensina e todo mundo esquece

NovoJS

Em página estática, esquecer de limpar não dói: a página recarrega e tudo some. O problema aparece em site com troca de conteúdo sem recarregar — e no seu caso, mais cedo do que parece, porque a View Transition do CSS não recarrega o JavaScript.

js
// guarde TODAS as funções de limpeza
const limpezas = [];

limpezas.push(inView('.secao', aparecer));
limpezas.push(hover('.card', levantar));
limpezas.push(scroll(atualizarBarra));

function desmontar() {
  limpezas.forEach(f => f());
  limpezas.length = 0;
}
O sintoma de não limpar é característico: a página vai ficando lenta conforme o visitante navega, e o celular esquenta. São observadores antigos ainda rodando sobre elementos que nem existem mais.

Arrastar

NovoJS

Aqui a versão JavaScript e a versão React se separam bastante. Em React, arrastar é uma propriedade — drag, dragConstraints, dragElastic, dragMomentum, e ao soltar o motor de inércia entra sozinho. Em JavaScript puro, você monta o gesto na mão e usa o Motion para a parte animada.

js — esqueleto do gesto
let arrastando = false, inicioX = 0, atualX = 0, ultimoT = 0, velocidade = 0;

alvo.addEventListener('pointerdown', e => {
  arrastando = true; inicioX = e.clientX - atualX;
  alvo.setPointerCapture(e.pointerId);
});

alvo.addEventListener('pointermove', e => {
  if (!arrastando) return;
  const anterior = atualX;
  atualX = e.clientX - inicioX;
  const agora = performance.now();
  velocidade = (atualX - anterior) / ((agora - ultimoT) || 16) * 1000;
  ultimoT = agora;
  alvo.style.transform = `translateX(${atualX}px)`;
});

alvo.addEventListener('pointerup', () => {
  arrastando = false;
  animate(alvo, { x: 0 }, { type: 'inertia', velocity, min: -600, max: 0 });
});
Arraste e solte — ele volta com molaresistência depois de 120px
Antes de construir isso: para carrossel de fotos, overflow-x: auto com scroll-snap-type: x mandatory resolve em quatro linhas de CSS, funciona com teclado, funciona com leitor de tela e tem inércia nativa do sistema. Gesto de arrastar feito à mão quase sempre fica pior que o nativo — e custa um dia.

A tabela honesta desta parte

Novo
Você querUsePor quê
Revelar seção ao rolarCSS animation-timeline: view()Nativo, zero JavaScript, degrada sozinho
Revelar com cascata de N itensinView + staggerÉ o caso que o CSS não cobre
Card levantando no hoverCSSUma linha, sem dependência
Botão com retorno de molapress + springMola o CSS não faz
Vídeo que toca ao aparecerinView com retornoPrecisa de JavaScript de qualquer jeito
Carrossel arrastávelCSS scroll-snapNativo é melhor que qualquer reimplementação
Quatro das seis linhas apontam para o CSS. Isso não é argumento contra o Motion — é o argumento a favor de usá-lo só nas duas linhas em que ele é insubstituível. Biblioteca boa é a que você usa pouco e no lugar certo.
PARTE 7

SVG, 3D e valores soltos

Três territórios que não são o dia a dia, mas que resolvem pedidos específicos: o traço que se desenha sozinho, a profundidade, e a animação de um número que não tem elemento nenhum por trás.

O traço que se desenha

NovoJS

O efeito mais reconhecível em SVG: uma linha, um ícone ou uma assinatura que aparece como se estivesse sendo desenhada. O truque real é antigo — stroke-dasharray e stroke-dashoffset —, e o Motion embrulha isso em nomes que você entende.

js
animate('.assinatura path',
  { pathLength: [0, 1] },
  { duration: 1.6, ease: 'easeInOut' }
);

// pathLength   → quanto do traço está visível, de 0 a 1
// pathOffset   → desloca o trecho visível ao longo da linha
// pathSpacing  → o espaço entre os pedaços, quando tracejado
e no SVG
<svg viewBox="0 0 200 80" fill="none" aria-hidden="true">
  <path d="M10 60 Q 60 10 110 45 T 190 30"
        stroke="var(--accent)" stroke-width="3" stroke-linecap="round"/>
</svg>
O traço que se desenhapathLength="1"
Onde isso ganha dinheiro: logotipo desenhando na hero, um traço sublinhando a palavra-chave da chamada, o contorno de um ícone se formando ao entrar na tela. É barato — um SVG pesa alguns KB — e passa impressão de acabamento caro. Use uma vez por página.

O que não dá para fazer com SVG aqui

Novo

Morfar uma forma em outra não é suportado. Transformar um círculo num raio, uma seta num X, o contorno de um estado no contorno de outro — isso exige interpolar o próprio comando do caminho, e é um problema difícil: os dois caminhos precisam ter o mesmo número de pontos, na mesma ordem.

Você querCaminho
Traço se desenhandoMotion, com pathLength
Mover, girar, escalar um SVGMotion, normalmente — SVG aceita transform
Animar cor, opacidade, espessura do traçoMotion, pelo nome da propriedade
Uma forma virando outraPlugin de morph do GSAP, ou trocar de ícone com fade
Ícone de menu virando XCSS — são três barras girando, não um morph
A última linha vale registrar: o "hambúrguer que vira X" parece morph e não é. São três retângulos: o do meio some, os outros dois giram 45 graus em direções opostas e se centralizam. Doze linhas de CSS, sem biblioteca.

Profundidade

NovoJS
js + css
/* a perspectiva vive no PAI — sem ela, o 3D achata e nada acontece */
.palco { perspective: 900px; }
.carta { transform-style: preserve-3d; }

animate('.carta', { rotateY: 180 }, { type: 'spring', bounce: 0.2 });

animate('.carta', {
  rotateX: -12,
  rotateY: 18,
  z: 40,                      // aproxima do observador
  transformPerspective: 800   // perspectiva no próprio elemento
}, { duration: 0.4 });
Valor de perspectiveEfeito
300–600pxDramático, quase caricato
800–1200pxNatural — é a faixa que você quer
2000px+Sutil a ponto de quase não existir
Dois erros clássicos: esquecer a perspective no pai (e concluir que "não funciona"), e esquecer backface-visibility: hidden na carta que vira — sem isso, o verso aparece espelhado no meio do giro.

Animar um valor que não é elemento

NovoJS

O recurso que mais rende em site de serviço, e o menos lembrado. Você anima um número e decide o que fazer com ele a cada quadro.

js
// número simples
animate(0, 248, {
  duration: 1.4, ease: 'easeOut',
  onUpdate: v => el.textContent = Math.round(v)
});

// valor com formato brasileiro
animate(0, 18500, {
  duration: 1.2,
  onUpdate: v => el.textContent = v.toLocaleString('pt-BR', {
    style: 'currency', currency: 'BRL', maximumFractionDigits: 0
  })
});

// entre duas cores, sem elemento
animate('#305546', '#B8461F', {
  duration: 1,
  onUpdate: cor => document.body.style.setProperty('--destaque', cor)
});

// um objeto inteiro de uma vez
const estado = { x: 0, opacidade: 0 };
animate(estado, { x: 100, opacidade: 1 }, {
  duration: 0.6,
  onUpdate: () => desenharNoCanvas(estado)
});
O uso que eu mais recomendo: contador de obras entregues, de anos de atuação, de clientes atendidos. É o número que o cliente quer destacar, ele sobe em um segundo e meio, e custa dez linhas. E como é só texto, funciona com leitor de tela se você deixar o valor final também escrito no HTML.

Animar variáveis CSS

NovoCSS

Uma ponte elegante entre o JavaScript e o seu design system: em vez de animar cada elemento, você anima um token e o CSS distribui.

js
animate('.secao', { '--intensidade': [0, 1] }, { duration: 0.8 });
css
/* declare o tipo — sem isso o navegador trata como texto e não interpola */
@property --intensidade {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.secao      { background: rgb(48 85 70 / calc(var(--intensidade) * 0.12)); }
.secao .cta { transform: scale(calc(1 + var(--intensidade) * 0.04)); }
Um valor animado, vários elementos reagindo — e a lógica de aparência continua toda no CSS, onde ela deve estar. É o padrão que mantém o JavaScript magro à medida que o efeito cresce.
PARTE 8

O equivalente em React

Você não usa React hoje. Esta parte existe por dois motivos: para você entender o material que encontrar na internet, que em maioria é React, e para o dia em que um projeto justificar a mudança — o vocabulário é o mesmo, só a embalagem muda.

De função para propriedade

NovoJS

A diferença de fundo: em JavaScript você manda animar; em React você descreve o estado e a biblioteca anima a diferença.

js — o que você já sabe
animate('.card', { opacity: [0, 1], y: [20, 0] }, { duration: 0.5 });
react — o mesmo resultado
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  …
</motion.div>

As propriedades principais, agrupadas por função:

GrupoPropriedades
Estadosinitial, animate, exit, transition, variants
GestowhileHover, whileTap, whileFocus, whileDrag
RolagemwhileInView, viewport, onViewportEnter, onViewportLeave
Arrastardrag, dragConstraints, dragElastic, dragMomentum, dragSnapToOrigin
Layoutlayout, layoutId, layoutDependency, layoutRoot
EventosonAnimationStart, onAnimationComplete, onUpdate
Repare que transition aceita exatamente as mesmas opções que você aprendeu na Parte 2 e na Parte 3: duration, ease, type: 'spring', bounce, stiffness. Nada disso precisa ser reaprendido.

Os dois recursos que nasceram no React

NovoJS

Animação de layout. Com layout, o elemento anima sozinho quando a posição ou o tamanho dele muda por qualquer motivo — um filtro reordenou a lista, um acordeão abriu, a janela mudou de tamanho. Isso é difícil de fazer à mão porque exige medir antes e depois e inverter a diferença.

react
{itensFiltrados.map(i => (
  <motion.li key={i.id} layout>{i.nome}</motion.li>
))}
// reordenou? cada item desliza para a posição nova, sozinho

Elemento compartilhado. Com o mesmo layoutId em dois componentes diferentes, a biblioteca entende que é "a mesma coisa" e faz um transitar no outro — a miniatura que cresce e vira a foto aberta.

react
<motion.img layoutId={`foto-${obra.id}`} src={obra.mini} />
// e, no modal aberto:
<motion.img layoutId={`foto-${obra.id}`} src={obra.grande} />
E o equivalente em HTML puro: desde junho de 2026, o Motion em JavaScript puro faz os dois com o animateView, no próximo tópico. E, para o segundo caso, existe ainda view-transition-name no CSS, nativo, que faz um elemento "viajar" entre duas páginas. Não é tão flexível, mas resolve o caso principal — a foto do card virando a foto da página do projeto — sem framework nenhum.

animateView: o layout animado, agora sem React

NovoJS

Desde a versão 12.41, de junho de 2026, o Motion em JavaScript puro tem o animateView. Ele faz, com a View Transitions API do próprio navegador, o que o layout faz no React: você muda o DOM do jeito que quiser, e os elementos marcados deslizam da posição antiga para a nova — sem medir nada, sem calcular posição.

A função que você passa só muda a página. O resto é encadeado: .add() diz quais elementos acompanhar, e os métodos seguintes valem para eles.

js — filtro de cardápio ou portfólio
const { animateView } = Motion;
const lista = document.querySelector('.catalogo');

// só muda o DOM: esconde o que não é da categoria
function filtrar(categoria) {
  for (const item of lista.children) {
    item.hidden = categoria !== 'tudo' && item.dataset.cat !== categoria;
  }
}

function aoClicar(categoria) {
  animateView(() => filtrar(categoria), { duration: 0.4 })
    .add('.catalogo > li')        // cada item ganha um view-transition-name
    .enter({ opacity: [0, 1] })    // os que aparecem
    .exit({ opacity: 0 });         // os que somem
  // os que ficam deslizam sozinhos para o lugar novo
}
MétodoFaz
.add(alvo)Marca os elementos (seletor ou elemento) para acompanhar a mudança
.add(de, para)Um elemento que vira outro — a miniatura que vira a foto grande
.layout(opcoes)A curva e a duração do deslocamento
.enter() · .exit()O que só aparece e o que só some na mudança
.new() · .old()A imagem nova e a antiga de um mesmo elemento, cada uma com a sua animação
.class(nome)Dá um view-transition-class para você estilizar no CSS
.crop(false) · .group(false)Desligam o recorte automático e o agrupamento pelo DOM

As opções de tempo são as mesmas da Parte 3, inclusive mola: animateView(mudar, { type: 'spring', bounce: 0.3 }). A chamada devolve o mesmo objeto de controle do animate, com pause(), cancel() e finished.

Três cuidados. Onde o navegador não tem View Transitions, a mudança acontece sem animação — o filtro continua funcionando, só não desliza. Uma transição de tela por vez: se o visitante clicar de novo no meio, o Motion enfileira a próxima em vez de deixar a tela quebrar. E a documentação não fala de "reduzir movimento", então faça você, como na Parte 2: com prefers-reduced-motion: reduce, chame filtrar() direto, sem o animateView.

Mover em arco: arc() — por enquanto, só no React

NovoJS

Desde a 12.40, de maio de 2026, o Motion tem arc(): em vez de ir em linha reta de um ponto a outro, o elemento faz uma curva, como um objeto jogado. Ele entra na opção path da transição e vale para x e y — e para a animação de layout, que é onde mais aparece: a miniatura que voa em curva até virar a foto aberta.

react
import { arc, motion } from "motion/react";

// crie uma vez, fora do componente: reaproveitar deixa a interrupção mais suave
const emArco = arc({ strength: 0.5 });

<motion.div
  animate={{ x: 200, y: 100 }}
  transition={{ duration: 0.8, path: emArco }}
/>
OpçãoPadrãoFaz
strength0.5Quanto a curva se abre. 0 é linha reta
peak0.5Onde fica o ponto mais alto do arco. 0.5 é no meio
directionautomático'cw' (sentido horário) ou 'ccw' (anti-horário)
rotatefalsetrue gira o elemento acompanhando a curva
Por que está na parte de React. A documentação do Motion diz que o arc() também funciona com o animate() do JavaScript puro. Testamos em 29 de setembro de 2026, na versão 13.4.6, e não funciona: o elemento vai em linha reta, sem erro nenhum. O código-fonte explica — no JavaScript puro, o animate() de um elemento comum anima direto no estilo, sem o objeto intermediário que o path usa. Enquanto isso não mudar, arco em HTML puro sai de dois jeitos: dar uma curva diferente para cada eixo ({ x: { ease: 'linear' }, y: { ease: 'easeOut' } }, como na Parte 2) ou usar offset-path, o caminho de movimento do próprio CSS.

Os hooks, em uma passada

NovoJS
HookFazEquivalente que você já viu
useMotionValueUm valor animável que não recria o componenteUma variável comum com onUpdate
useScrollProgresso da rolagem como valorscroll() da Parte 5
useTransformConverte uma faixa em outraUma conta no onUpdate
useSpringSuaviza um valor com molaanimate com type: 'spring'
useAnimateDevolve o animate preso a um escopoO próprio animate
AnimatePresencePermite animar a saída antes de removerNão existe em JS puro sem gambiarra
react — parallax completo em cinco linhas
const { scrollYProgress } = useScroll({ target: ref });
const y = useTransform(scrollYProgress, [0, 1], [0, -60]);
const suave = useSpring(y, { stiffness: 120, damping: 20 });

<motion.img ref={ref} style={{ y: suave }} />
AnimatePresence é o único da lista sem equivalente honesto em HTML puro. Animar a saída de um elemento antes de removê-lo exige adiar a remoção até a animação acabar — em JavaScript puro você faz isso na mão, com await na animação antes do remove().

Quando migrar — e quando não

Novo
SituaçãoVale React?
Landing page de serviçoNão. Nem de longe.
Site institucional de 6 páginasNão — se o problema é repetição, veja a Parte 7 da trilha Fluxo de trabalho
Catálogo com filtro e reordenação animadaNão mais só por isso — o animateView faz a reordenação animada em HTML puro
Galeria com miniatura abrindo em modalNão necessariamente — View Transitions cobre boa parte
Painel com muito estado que mudaSim, mas aí a razão é o estado, não a animação
A pergunta de sempre: a animação nunca é motivo suficiente para adotar um framework. Se o React entrar no seu fluxo, que seja porque a interface tem estado complexo demais para o DOM na mão — e aí a animação vem de brinde, com a mesma API que você já conhece.

O mapa da API, numa página

Novo
referência rápida
// ── as cinco funções ────────────────────────────────────────────
animate(alvo, propriedades, opcoes)      → objeto de controle
animate(de, para, opcoes)                → anima um valor solto
animate([ [alvo, props, {at}], … ])      → sequência
scroll(callback | animacao, opcoes)      → função de limpeza
inView(alvo, callback, opcoes)           → função de limpeza
hover(alvo, callback)                    → função de limpeza
press(alvo, callback)                    → função de limpeza
stagger(segundos, {startDelay, from, ease})  → gerador de delay
animateView(mudarDOM, opcoes)            → .add() .enter() .exit() .layout()

// ── opções de tempo ─────────────────────────────────────────────
duration  delay  repeat  repeatType  repeatDelay  times  ease
type: 'tween' | 'spring' | 'inertia'
spring novo:   bounce, visualDuration
spring físico: stiffness, damping, mass, velocity, restDelta
por propriedade: { opacity: { duration: .2 }, y: { type:'spring' } }
path: arc({ strength, peak, direction, rotate })   (só React, por enquanto)

// ── curvas ──────────────────────────────────────────────────────
linear  easeIn/Out/InOut  backIn/Out/InOut  circIn/Out/InOut
anticipate  cubicBezier([a,b,c,d])  steps(n)  função própria

// ── objeto de controle ──────────────────────────────────────────
play()  pause()  cancel()  complete()  stop()
time  speed  duration  then()

// ── scroll ──────────────────────────────────────────────────────
{ container, axis:'x'|'y', target, offset:['start end','end start'],
  trackContentSize }
callback(progresso, info) → info.y.{current,progress,velocity,scrollLength}

// ── inView ──────────────────────────────────────────────────────
{ root, margin:'0px 0px -15% 0px', amount:'some'|'all'|0..1 }
retorne uma função → ela roda quando o elemento SAI

// ── SVG e 3D ────────────────────────────────────────────────────
pathLength  pathOffset  pathSpacing
rotateX/Y/Z  z  transformPerspective   (perspective vai no pai)
Esta é a folha para deixar aberta enquanto trabalha. O resto da trilha é sobre quando usar cada coisa — que é a parte que a documentação de qualquer biblioteca, por natureza, não pode responder por você.