Motion Foundations
La capa base del sistema de movimiento. Define cada valor, restricción y
regla que heredan las habilidades secundarias (motion-patterns, motion-advanced).
Carga esta habilidad antes de comenzar cualquier trabajo de animación.
Cuándo activarla
- Al comenzar cualquier componente animado desde cero
- Al configurar tokens, presets de resorte o valores de easing
- Al implementar soporte para
prefers-reduced-motion - Al depurar discrepancias de hidratación causadas por estados iniciales de animación
- Al evaluar si una animación debería existir
Salidas
Esta habilidad produce:
- Un objeto
motionTokenscompartido (duración, easing, distancia, escala) - Un mapa de presets
springscompartido (5 configuraciones con nombre) - Una puerta
shouldAnimate()utilizada por todos los componentes - Valores predeterminados de animación accesibles mediante
useReducedMotion - Estados iniciales seguros para SSR sin advertencias de hidratación
Principios
El movimiento debe cumplir al menos uno de los siguientes, o debe eliminarse:
- Guiar la atención
- Comunicar estado
- Preservar la continuidad espacial
La capacidad de respuesta siempre prevalece sobre la fluidez. Una animación de 60 fps que causa retraso en la entrada es peor que ninguna animación.
Reglas
Estas no son negociables. Se aplican a todos los componentes del sistema.
- Usa solo
motion/react. Nunca importes desdeframer-motion. No mezcles ambos en el mismo árbol. initialdebe coincidir con la salida del servidor. Si el servidor renderizaopacity: 1, la propinitialtambién debe seropacity: 1. Sin excepciones.- El movimiento reducido lo anula todo. Cuando
useReducedMotion()devuelvetrueoprefersReducedestrue, todas las transformaciones están deshabilitadas. Solo se permiten fundidos de opacidad de ≤ 0.2 s. - Nunca animar propiedades de layout.
width,height,top,left,margin,paddingestán prohibidas enanimate. Usa solotransformyopacity. - Todos los valores de token provienen de
motionTokens. Las duraciones y easings hardcodeados en archivos de componente están prohibidos. - Todas las configuraciones de resorte provienen del mapa
springs. Los valores inline destiffness/dampingestán prohibidos. "use client"es obligatorio en cada archivo que importe demotion/react.- Nunca leas
windowonavigatora nivel de módulo. Siempre protege contypeof window !== "undefined".
Guía de decisión
Elegir una duración
| Token | Usar cuando |
|---|---|
instant | Mostrar/ocultar tooltip, anillo de enfoque, actualización de badge |
fast | Retroalimentación de botón, cambio de icono, alternancia de chip |
normal | Apertura de modal, expansión de tarjeta, entrada de elemento de página |
slow | Entrada de héroe, transición de página completa |
crawl | Narración deliberada; usar con moderación |
Elegir un resorte
| Preset | Usar cuando |
|---|---|
snappy | UI predeterminado: botones, chips, elementos de navegación |
gentle | Tarjetas, modales, paneles que aterrizan suavemente |
bouncy | Momentos lúdicos: estados vacíos, incorporación |
instant | Tooltips, popovers, menús desplegables |
release | Liberación de arrastre: sensación física natural |
Cuándo deshabilitar la animación por completo
Deshabilitar (hacer que shouldAnimate() devuelva false) cuando:
prefersReducedestrueisLowEndestruey la animación no es esencial- El elemento está fuera de pantalla y nunca entrará en el viewport
- La animación es puramente decorativa sin propósito de UX
Conceptos centrales
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 },
}
Indicadores de tiempo de ejecución
// 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
},
}
Accesibilidad
Orden de prioridad (de mayor a menor):
prefers-reduced-motion: reduce: deshabilita todas las transformaciones, limita las transiciones de opacidad a ≤ 0.2 s- Detección de dispositivos de gama baja: reduce la duración, elimina animaciones no esenciales
- Preferencia de diseño: todo lo demás
El movimiento debe degradarse gradualmente. Nunca debe desaparecer abruptamente de una manera que cause cambios de layout o desoriente al usuario.
// 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>
Seguridad SSR / hidratación
Regla: initial siempre debe coincidir con lo que renderiza el servidor.
// INCORRECTO: el servidor renderiza opacity:1 pero initial dice 0 → error de hidratación
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECTO: usar AnimatePresence o diferir al montaje del cliente
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Ejemplos de código
Completo: tokens + resortes + accesibilidad + protección 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) {
// Protección SSR: inicial debe coincidir con la salida del servidor (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accesibilidad: deshabilita transform cuando se prefiere movimiento reducido
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Puerta de dispositivo: omitir animación en hardware de gama baja
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>
)
}
Restricciones / No objetivos
Esta habilidad no cubre:
- Patrones de componentes de UI (botón, modal, escalonado) → ver
motion-patterns - Arrastre, gestos, SVG, animaciones de texto, hooks personalizados → ver
motion-advanced - Animaciones solo CSS o clases
animate-*de Tailwind sinmotion/react - Bibliotecas de animación de terceros (GSAP, anime.js, etc.)
- Decisiones de diseño de movimiento (cuándo animar, qué enfatizar) — eso es una preocupación de diseño, no una restricción de código
Anti-patrones
| Anti-patrón | Regla violada | Corrección |
|---|---|---|
import { motion } from "framer-motion" | Regla 1 | Usar motion/react |
initial={{ opacity: 0 }} en componente SSR | Regla 2 | Agregar guardia de montaje |
Omitir la verificación useReducedMotion | Regla 3 | Usar hook useSafeMotion |
animate={{ width: "100%" }} | Regla 4 | Usar transformación scaleX en su lugar |
transition={{ duration: 0.4 }} inline | Regla 5 | Usar motionTokens.duration.normal |
{ stiffness: 300, damping: 30 } inline | Regla 6 | Usar springs.snappy |
Falta la directiva "use client" | Regla 7 | Agregar al inicio del archivo |
navigator.hardwareConcurrency a nivel de módulo | Regla 8 | Envolver en typeof navigator !== "undefined" |
Habilidades relacionadas
motion-patterns: consume los tokens y resortes definidos aquí para construir patrones de botón, modal, escalonado, transición de página y desplazamiento. No redefine ningún valor.motion-advanced: consume los tokens y resortes definidos aquí para patrones de arrastre, SVG, texto y gestos. Agrega secuenciasuseAnimatey hooks personalizados sobre esta base.