Fundamentos de Motion
La capa base del sistema de movimiento. Define cada valor, restricción y
regla que las habilidades secundarias (motion-patterns, motion-advanced) heredan.
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 spring o valores de easing
- Al implementar soporte para
prefers-reduced-motion - Al depurar errores de hidratación causados por estados iniciales de animación
- Al evaluar si una animación debería existir siquiera
Resultados
Esta habilidad produce:
- Un objeto compartido
motionTokens(duración, easing, distancia, escala) - Un mapa compartido de presets
springs(5 configuraciones nombradas) - Una compuerta
shouldAnimate()utilizada por todos los componentes - Valores predeterminados de animación compatibles con accesibilidad mediante
useReducedMotion - Estados iniciales seguros para SSR sin advertencias de hidratación
Principios
El movimiento debe cumplir al menos uno de los siguientes criterios, 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 a 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. Nunca 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 se deshabilitan. Solo se permiten desvanecimientos de opacidad con una duración ≤ 0,2 s. - Nunca animes 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 los archivos de componentes están prohibidos. - Todas las configuraciones de spring provienen del mapa
springs. Los valores inline destiffness/dampingestán prohibidos. - Se requiere
"use client"en cada archivo que importe desdemotion/react. - Nunca leas
windowonavigatora nivel de módulo. Siempre protégete contypeof window !== "undefined".
Guía de decisión
Elección de una duración
| Token | Úsalo cuando |
|---|---|
instant | Mostrar/ocultar tooltip, anillo de enfoque, actualización de insignia |
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; úsalo con moderación |
Elección de un spring
| Preset | Úsalo cuando |
|---|---|
snappy | Interfaz de usuario predeterminada: botones, chips, elementos de navegación |
gentle | Tarjetas, modales, paneles que aterrizan suavemente |
bouncy | Momentos divertidos: estados vacíos, incorporación |
instant | Tooltips, popovers, menús desplegables |
release | Liberación de arrastre: sensación de física natural |
Cuándo deshabilitar la animación por completo
Deshabilita (haz 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 básicos
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 },
}
Banderas 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: desactiva 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 con gracia. Nunca debe desaparecer abruptamente de una manera que cause cambios de layout o desoriente.
// 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 el servidor renderiza.
// INCORRECTO: el servidor renderiza opacity:1 pero initial dice 0 → error de hidratación
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECTO: usa AnimatePresence o difiere 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
De principio a fin: tokens + springs + accesibilidad + guardia 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) {
// Guardia SSR: initial debe coincidir con la salida del servidor (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accesibilidad: desactiva la transformación cuando se prefiere movimiento reducido
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Compuerta de dispositivo: salta la 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) → consulta
motion-patterns - Arrastre, gestos, SVG, animaciones de texto, hooks personalizados → consulta
motion-advanced - Animaciones solo con 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 | Usa motion/react |
initial={{ opacity: 0 }} en componente SSR | Regla 2 | Agrega guardia de montaje |
Omitir la verificación de useReducedMotion | Regla 3 | Usa el hook useSafeMotion |
animate={{ width: "100%" }} | Regla 4 | Usa la transformación scaleX en su lugar |
transition={{ duration: 0.4 }} inline | Regla 5 | Usa motionTokens.duration.normal |
{ stiffness: 300, damping: 30 } inline | Regla 6 | Usa springs.snappy |
Falta la directiva "use client" | Regla 7 | Agrega al inicio del archivo |
navigator.hardwareConcurrency a nivel de módulo | Regla 8 | Envuelve con typeof navigator !== "undefined" |
Habilidades relacionadas
motion-patterns: consume los tokens y springs 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 springs definidos aquí para patrones de arrastre, SVG, texto y gestos. Agrega secuenciasuseAnimatey hooks personalizados sobre esta base.