Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · Motion: animacao JS declarativa, View Transitions, Rive e a11y · 0/7
Recomendado: essencial

Padroes de animacao: layout, gestures, drag, scroll-linked

7 min de leitura

fonte

Voce ja sabe fazer fade in, AnimatePresence, variants. Agora vem a parte que faz Motion valer a pena sobre CSS: animacoes que reagem a estado do DOM (FLIP, layout), a gesto do usuario (drag, swipe), ou a posicao de scroll (parallax, progress bar). Esses 3 padroes - layout, drag, useScroll - cobrem 90% das animacoes "avancadas" que uma landing page, dashboard ou app moderno precisa.

Se voce entende layout + drag + useScroll

  • useTransform, voce consegue resolver reordenacao de lista, swipe-to-dismiss, parallax de hero, progress bar de scroll, e shared element transitions (foto vira hero).

O essencial 🟢

layout prop - FLIP animation automatica. A prop layout faz Motion animar a posicao e tamanho de um elemento quando eles mudam. Por baixo dos panos, Motion aplica o algoritmo FLIP (First, Last, Invert, Play):

<motion.div layout>
  Conteudo
</motion.div>

O que muda na pratica: imagine uma lista de cards que reordena. Sem layout, os cards "pulam" pra nova posicao instantaneamente. Com layout, eles deslizam suavemente:

function CardList({ items, onReorder }) {
  return (
    <ul>
      {items.map((item) => (
        <motion.li
          key={item.id}
          layout              // <-- FLIP automatico
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        >
          {item.text}
        </motion.li>
      ))}
    </ul>
  );
}

Ao reordenar, Motion mede a posicao antiga e nova de cada item, calcula o delta, e anima com spring. Voce nao precisa escrever nenhum codigo de animacao - a prop layout faz tudo.

Quando layout ajuda. Em qualquer caso em que um elemento muda de tamanho ou posicao no DOM:

  • Items de lista sendo adicionados/removidos.
  • Sidebar que abre/fecha (largura muda).
  • Modal que expande (altura cresce).
  • Reordenacao (drag-and-drop reorder).

Em todos esses casos, sem layout, o usuario ve "pular". Com layout, ve "transitar".

layoutId - shared layout entre elementos. Quando o "mesmo item" existe em dois lugares diferentes da arvore, voce quer que ele "viaje" de um lugar pro outro em vez de sumir e reaparecer. layoutId e a chave pra isso:

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

function Gallery() {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <>
      {/* Lista */}
      <motion.div layoutId={selectedId ?? "placeholder"}>
        {/* item da lista */}
      </motion.div>

      {/* Hero - aparece quando selecionado */}
      <AnimatePresence>
        {selectedId && (
          <motion.div
            layoutId={selectedId}
            onClick={() => setSelectedId(null)}
          >
            {/* versao expandida do mesmo item */}
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}

O elemento "cresce" da posicao da lista pra posicao do hero. Visualmente: o card "voa" da galeria pro hero. E o pattern usado no Unsplash, Apple website, e em todos os portfolios modernos.

layoutId precisa de AnimatePresence ou LayoutGroup por volta. O AnimatePresence garante que Motion mantem o elemento no DOM durante a transicao entre as duas "versoes".

drag prop - arrastar com o mouse/dedo. A prop drag faz o elemento responder a gestos de arrasto. Sem voce escrever onMouseDown/onMouseMove/onMouseUp:

<motion.div
  drag                      // arrastar no eixo X e Y
  dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
  dragElastic={0.1}         // resistencia nas bordas (0 = rigido, 1 = elastico)
  dragMomentum={true}       // inercia ao soltar (default true)
  onDragEnd={(event, info) => console.log(info.offset.x, info.offset.y)}
>
  Arraste-me
</motion.div>

drag="x" restringe ao eixo X, drag="y" ao Y. drag (sem valor) ativa os dois eixos. Os 4 modificadores mais uteis:

  • dragConstraints: limite de movimento (numerico {left, right, top, bottom} ou ref a um container).
  • dragElastic: quanto "elastico" alem do constraint (0 = trava, 1 = super elastico).
  • dragMomentum: se solta com inercia (default true).
  • dragSnapToOrigin: se volta pra posicao inicial ao soltar (util pra "swipe to dismiss").

dragConstraints com ref - limite a partir de um container. Em vez de calcular numeros manuais, voce passa uma ref a um container e Motion limita o arrasto dentro dele:

import { useRef } from "react";
import { motion } from "motion/react";

function DraggableCard() {
  const containerRef = useRef(null);

  return (
    <div ref={containerRef} style={{ width: 400, height: 300, position: "relative" }}>
      <motion.div
        drag
        dragConstraints={containerRef}    // <-- limita ao container
        dragElastic={0.2}
      >
        Card arrastavel
      </motion.div>
    </div>
  );
}

Motion mede o tamanho do container e do card automaticamente. O card nao sai do container. Mais robusto que numeros hardcoded.

useScroll - posicao de scroll como MotionValue. O hook useScroll retorna a posicao de scroll como MotionValue (valor animavel fora do React):

import { motion, useScroll, useTransform } from "motion/react";

function ScrollProgressBar() {
  const { scrollYProgress } = useScroll();
  // scrollYProgress vai de 0 (topo) a 1 (fim da pagina)

  return (
    <motion.div
      style={{
        scaleX: scrollYProgress,    // barra cresce com o scroll
        transformOrigin: "left",
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        height: 4,
        background: "blue",
      }}
    />
  );
}

O scrollYProgress atualiza em tempo real durante o scroll, sem re-render do React. A barra de progresso cresce conforme o usuario rola. Padrao classico de "read progress" em artigos.

useScroll pode apontar pra um container especifico. Em vez de scrollar a pagina, voce observa um container:

const targetRef = useRef(null);
const { scrollYProgress } = useScroll({
  target: targetRef,
  offset: ["start end", "end start"],
});

offset: ["start end", "end start"] significa "a animacao comeca quando o topo do target atinge o fim do viewport, e termina quando o fim do target atinge o inicio do viewport". E o jeito de fazer animacoes "triggered by scroll" tipo parallax.

useTransform - mapear um MotionValue pra outro. O hook useTransform pega um MotionValue e mapeia via ranges de entrada/saida:

import { motion, useScroll, useTransform } from "motion/react";

function ParallaxHero() {
  const { scrollY } = useScroll();
  // scrollY vai de 0 a 800px (altura do hero)

  // y do background vai de 0 a -200 (move pra cima mais devagar)
  const yBackground = useTransform(scrollY, [0, 800], [0, -200]);
  // y do foreground vai de 0 a -100 (move menos)
  const yForeground = useTransform(scrollY, [0, 800], [0, -100]);

  return (
    <>
      <motion.div style={{ y: yBackground, position: "absolute" }}>
        Background image
      </motion.div>
      <motion.div style={{ y: yForeground, position: "absolute" }}>
        Foreground content
      </motion.div>
    </>
  );
}

Background move 200px, foreground move 100px -> parallax. Voce construiu parallax de hero sem nenhuma math manual, sem nenhum listener de scroll, sem re-render do React. E so Motion.

useTransform com 3+ valores pra "pulsar". Se a saida e maior que a entrada, Motion interpola entre os pontos. Pra fazer algo "pulsar" (sai de 0, sobe pra 1, volta pra 0):

const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 0.5, 1]);

Aos 0% de scroll: opacity 1. Aos 50%: 0.5. Aos 100%: 1 de novo. Pattern classico de "fade no meio" em hero sections.

useMotionValue - valor customizado animavel. Em 90% dos casos voce vai usar useScroll (que retorna MotionValue pronto). Mas pra criar o seu:

import { motion, useMotionValue, useTransform } from "motion/react";

function CustomValue() {
  const x = useMotionValue(0);     // MotionValue inicializado em 0
  const opacity = useTransform(x, [-100, 0, 100], [0, 1, 0]);

  return (
    <motion.div style={{ x, opacity }}>
      Conteudo
    </motion.div>
  );
}

x e uma MotionValue (objeto especial que guarda um valor e dispara updates sem re-render). Voce passa pro style e Motion liga direto no DOM. Voce pode ler com x.get(), escrever com x.set(50), e usar em useTransform como fonte.

Aprofundamento 🟡

Por que FLIP existe. Antes do FLIP, animar a posicao de um elemento depois que ele mudou exigia: medir posicao antiga, mudar posicao, medir nova, calcular delta, aplicar transform inverso, animar pra zero. O FLIP e um algoritmo (Aerotwist, 2016) que sistematiza isso em 4 passos: First (mede posicao inicial), Last (muda layout, mede posicao final), Invert (aplica transform pra elemento "parecer" na posicao antiga), Play (anima transform pra zero). O Motion implementa isso em Rust/WebAssembly pra ser performante mesmo com 100+ items.

LayoutGroup - compartilhar layout entre sub-arvores. Quando dois elementos estao em componentes separados (nao compartilham ancestral direto), LayoutGroup ativa o compartilhamento de layoutId:

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

function App() {
  return (
    <LayoutGroup>
      <Sidebar />         {/* tem motion.div layoutId="avatar" */}
      <MainContent />     {/* tem outro motion.div layoutId="avatar" */}
    </LayoutGroup>
  );
}

Sem LayoutGroup, cada layoutId e unico dentro do seu ancestral mais proximo, e dois layoutId="avatar" em lugares diferentes colidiriam. Com LayoutGroup, voce declara "esses dois lugares compartilham o mesmo namespace de layoutId".

layout com Layout strategy customizada. A prop layout aceita um valor que controla o que e animado:

<motion.div
  layout="position"   // so posicao (default)
  // ou
  layout="size"       // so tamanho
  // ou
  layout              // ambos
/>

Em performance-cenas, voce pode preferir animar so posicao (e nao tamanho) pra evitar reflow. Conhecimento util pra listas com 100+ items.

whileDrag - estilo durante o arrasto. Assim como whileHover/whileTap, whileDrag aplica estilo enquanto o usuario arrasta:

<motion.div
  drag
  whileDrag={{ scale: 1.1, cursor: "grabbing" }}
  whileHover={{ cursor: "grab" }}
  whileTap={{ cursor: "grabbing" }}
>
  Arraste
</motion.div>

Combinado com whileHover e whileTap, voce constroi a UX de "pode arrastar" (cursor grab) -> "esta arrastando" (grabbing) sem escrever CSS.

useScroll com smooth (Motion 12+). Em vez de useScroll() que retorna valores raw de scroll, voce pode pedir valores "suavizados" (interpolados):

const { scrollY } = useScroll();
// scrollY: MotionValue que atualiza 1:1 com o scroll

// vs
const { scrollY } = useScroll({ smooth: 0.1 });
// scrollY: MotionValue suavizado com lag de 100ms

Util pra animacoes que "atrasam" o scroll (efeito smooth scroll sem mexer no scroll real do browser). Motion 12 introduziu isso.

useVelocity - velocidade de um MotionValue. Pra animacoes que dependem da velocidade (parallax com inercia, "rabicho" visual):

const x = useMotionValue(0);
const xVelocity = useVelocity(x);
const scale = useTransform(xVelocity, [-1000, 0, 1000], [0.5, 1, 0.5]);

Quando o usuario arrasta rapido, scale diminui (efeito "esticado"). Quando para, volta a 1. Pattern de games / efeitos fisicos.

Drag com snap a posicoes especificas. Pra drag que "encaixa" em pontos (timeline, carrossel):

<motion.div
  drag="x"
  dragSnapToOrigin={false}
  dragConstraints={{ left: 0, right: 300 }}
  onDragEnd={(event, info) => {
    if (info.offset.x > 150) snapToNext();
    else if (info.offset.x < -150) snapToPrev();
    else snapToOrigin();
  }}
/>

Padrao classico de carrossel mobile.

Pra quem quer ir mais alem 🔴

Por que layout requer position ou transform. O algoritmo FLIP usa transform: translate() pra mover o elemento visualmente sem causar reflow. Se o elemento tem position: static, Motion aplica position: relative automaticamente. Se voce tem position: absolute ou fixed, ele respeita. Mas se voce tem display: contents ou outra configuracao exotica, o FLIP pode quebrar. Em 95% dos casos funciona out-of-the-box.

useTransform avancado com clamp. Pra impedir que o output saia do range:

const xClamped = useTransform(x, [-100, 0, 100], [0, 0.5, 1], { clamp: true });
// xClamped sempre entre 0 e 1, mesmo se x sair de [-100, 100]

Util quando o x vem de useDrag e o usuario pode arrastar alem do esperado. Sem clamp, Motion extrapola (faz a animacao "continuar" proporcionalmente).

MotionValue.on() - subscribe manual. Pra reagir a um MotionValue sem hook React:

const x = useMotionValue(0);
useEffect(() => {
  return x.on("change", (latest) => {
    console.log("x mudou para:", latest);
  });
}, [x]);

Util pra integrar com bibliotecas imperativas (Three.js, GSAP, canvas). Em 90% dos casos, useTransform resolve sem precisar disso.

useMotionValueEvent - versao hook. Mais idiomatico em React:

import { useMotionValueEvent } from "motion/react";

const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
  console.log("x:", latest);
});

Limpa automaticamente o listener no unmount. Mesmo padrao de useEffect.

layout + AnimatePresence - "out" animation com FLIP. Padrao avancado pra items que saem de uma lista sendo reordenada:

<AnimatePresence>
  {items.map((item) => (
    <motion.li
      key={item.id}
      layout
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {item.text}
    </motion.li>
  ))}
</AnimatePresence>

O item que sai tem exit animation. Os que ficaram tem FLIP (deslizam pra nova posicao). Combinacao classica de "lista reordenavel com animacao de saida".

Leitura recomendada:

Dica: o erro mais comum em padroes de animacao e esquecer de passar layoutId identico nos dois elementos da shared transition. Se o layoutId for diferente ("avatar" vs "user-avatar"), Motion trata como elementos diferentes, sem animacao compartilhada. Regra: o mesmo layoutId aparece no elemento de origem E no destino.

No proximo no, vamos ver View Transitions API: a API nativa do browser (Chrome 111+) que faz transicoes de estado cross-page sem JS pesado, e o pattern de "theme toggle animado" com document.startViewTransition.

// Quiz

Qual a diferença prática entre `layout` e `layoutId` no Motion?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações