Fundamentos de Movimento
A camada base do sistema de movimento. Define todos os valores, restrições e
regras que as habilidades downstream (motion-patterns, motion-advanced) herdam.
Carregue esta habilidade antes de iniciar qualquer trabalho de animação.
Quando Ativar
- Iniciar qualquer componente animado do zero
- Configurar tokens, predefinições de mola ou valores de easing
- Implementar suporte a
prefers-reduced-motion - Depurar incompatibilidades de hidratação causadas por estados iniciais de animação
- Avaliar se uma animação deve existir
Saídas
Esta habilidade produz:
- Um objeto
motionTokenscompartilhado (duração, easing, distância, escala) - Um mapa de predefinições
springscompartilhado (5 configurações nomeadas) - Uma função
shouldAnimate()usada por todos os componentes - Padrões de animação em conformidade com acessibilidade via
useReducedMotion - Estados iniciais seguros para SSR sem avisos de hidratação
Princípios
O movimento deve cumprir pelo menos um dos seguintes ou deve ser removido:
- Guiar a atenção
- Comunicar estado
- Preservar continuidade espacial
Responsividade sempre supera suavidade. Uma animação de 60 fps que causa atraso na entrada do usuário é pior do que nenhuma animação.
Regras
Estas são inegociáveis. Aplicam-se a todos os componentes do sistema.
- Use
motion/reactapenas. Nunca importe deframer-motion. Nunca misture os dois na mesma árvore. initialdeve corresponder à saída do servidor. Se o servidor renderizaropacity: 1, a propinitialtambém deve seropacity: 1. Sem exceções.- Movimento reduzido substitui tudo. Quando
useReducedMotion()retornatrueouprefersReducedétrue, todas as transformações são desabilitadas. Fades apenas com opacidade em ≤ 0,2s são a única alternativa permitida. - Nunca anime propriedades de layout.
width,height,top,left,margin,paddingsão proibidos emanimate. Usetransformeopacityapenas. - Todos os valores de token vêm de
motionTokens. Durações e easings codificados em arquivos de componente são proibidos. - Todas as configurações de mola vêm do mapa
springs. Valores inline destiffness/dampingsão proibidos. "use client"é obrigatório em todo arquivo que importa demotion/react.- Nunca leia
windowounavigatorno nível do módulo. Sempre proteja comtypeof window !== "undefined".
Guia de Decisão
Escolhendo uma duração
| Token | Use quando |
|---|---|
instant | Exibir/ocultar tooltip, anel de foco, atualização de badge |
fast | Feedback de botão, troca de ícone, alternância de chip |
normal | Abertura de modal, expansão de card, entrada de elemento de página |
slow | Entrada de hero, transição de página inteira |
crawl | Narrativa deliberada; use com moderação |
Escolhendo uma mola
| Predefinição | Use quando |
|---|---|
snappy | UI padrão — botões, chips, itens de navegação |
gentle | Cards, modais, painéis pousando suavemente |
bouncy | Momentos lúdicos — estados vazios, onboarding |
instant | Tooltips, popovers, dropdowns |
release | Soltar ao arrastar — sensação física natural |
Quando desabilitar a animação completamente
Desabilite (faça shouldAnimate() retornar false) quando:
prefersReducedétrueisLowEndétruee a animação não é essencial- O elemento está fora da tela e nunca entrará no viewport
- A animação é puramente decorativa sem propósito de UX
Conceitos Principais
Sistema de tokens
// lib/motion-tokens.ts
export const motionTokens = {
duration: {
instant: 0.08,
fast: 0.18,
normal: 0.35,
slow: 0.6,
crawl: 1.0,
},
easing: {
smooth: [0.22, 1, 0.36, 1],
sharp: [0.4, 0, 0.2, 1],
bounce: [0.34, 1.56, 0.64, 1],
linear: [0, 0, 1, 1],
},
distance: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 48,
},
scale: {
subtle: 0.98,
press: 0.95,
pop: 1.04,
},
}
export const springs = {
snappy: { type: "spring", stiffness: 300, damping: 30 },
gentle: { type: "spring", stiffness: 120, damping: 14 },
bouncy: { type: "spring", stiffness: 400, damping: 10 },
instant: { type: "spring", stiffness: 600, damping: 35 },
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}
Flags de runtime
// lib/motion-config.ts
export const motionConfig = {
isLowEnd() {
return (
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4
)
},
prefersReduced() {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
},
shouldAnimate({ essential = false } = {}) {
if (this.prefersReduced()) return false
if (!essential && this.isLowEnd()) return false
return true
},
duration() {
return this.isLowEnd() || this.prefersReduced()
? motionTokens.duration.instant
: motionTokens.duration.normal
},
}
Acessibilidade
Ordem de prioridade (da maior para a menor):
prefers-reduced-motion: reduce— desabilita todas as transformações, limita transições de opacidade a ≤ 0,2s- Detecção de dispositivo de baixa performance — reduz duração, remove animações não essenciais
- Preferência de design — todo o resto
O movimento deve degradar-se graciosamente. Nunca deve desaparecer abruptamente de forma que cause mudança de layout ou confunda a orientação.
// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"
export function useSafeMotion(fullY: number = 16) {
const reduce = useReducedMotion()
return {
initial: { opacity: 0, y: reduce ? 0 : fullY },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: reduce ? 0 : -fullY },
}
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition { transition: opacity 0.15s; }
.motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
Segurança SSR / hidratação
Regra: initial deve sempre corresponder ao que o servidor renderiza.
// ERRADO — servidor renderiza opacity:1 mas initial diz 0 → incompatibilidade de hidratação
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRETO — use AnimatePresence ou adie para montagem do cliente
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Exemplos de Código
Ponta a ponta: tokens + molas + acessibilidade + guarda SSR
// components/fade-in-card.tsx
"use client"
import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"
interface FadeInCardProps {
children: React.ReactNode
delay?: number
}
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
// Guarda SSR — initial deve corresponder à saída do servidor (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Acessibilidade — desabilita transformação quando movimento reduzido é preferido
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Portão de dispositivo — pula animação em hardware de baixa performance
if (!motionConfig.shouldAnimate() || !mounted) {
return <div>{children}</div>
}
return (
<motion.div
initial={safeMotion.initial}
animate={safeMotion.animate}
exit={safeMotion.exit}
transition={{
...springs.gentle,
delay,
}}
whileHover={{ scale: motionTokens.scale.pop }}
whileTap={{ scale: motionTokens.scale.press }}
>
{children}
</motion.div>
)
}
Restrições / Não Objetivos
Esta habilidade não cobre:
- Padrões de componentes de UI (botão, modal, escalonamento) → veja
motion-patterns - Arrasto, gestos, SVG, animações de texto, hooks personalizados → veja
motion-advanced - Animações apenas CSS ou classes Tailwind
animate-*semmotion/react - Bibliotecas de animação de terceiros (GSAP, anime.js, etc.)
- Decisões de design de movimento (quando animar, o que enfatizar) — isso é uma preocupação de design, não uma restrição de código
Anti-Padrões
| Anti-padrão | Regra violada | Correção |
|---|---|---|
import { motion } from "framer-motion" | Regra 1 | Use motion/react |
initial={{ opacity: 0 }} em componente SSR | Regra 2 | Adicione guarda de montagem |
Pular verificação de useReducedMotion | Regra 3 | Use o hook useSafeMotion |
animate={{ width: "100%" }} | Regra 4 | Use transformação scaleX |
transition={{ duration: 0.4 }} inline | Regra 5 | Use motionTokens.duration.normal |
{ stiffness: 300, damping: 30 } inline | Regra 6 | Use springs.snappy |
Diretiva "use client" ausente | Regra 7 | Adicione ao topo do arquivo |
navigator.hardwareConcurrency no nível do módulo | Regra 8 | Envolva em typeof navigator !== "undefined" |
Habilidades Relacionadas
motion-patterns— consome tokens e molas definidos aqui para construir padrões de botão, modal, escalonamento, transição de página e rolagem. Não redefine nenhum valor.motion-advanced— consome tokens e molas definidos aqui para padrões de arrasto, SVG, texto e gestos. Adiciona sequênciasuseAnimatee hooks personalizados sobre esta base.