Motion para React (anteriormente Framer Motion) es una librería de animación declarativa que maneja fade-ins, efectos activados por scroll, animaciones de salida y transiciones de layout mediante props simples como initial, animate y exit — todo sincronizado con el estado de React.
Antes de esta librería, pasaba demasiado tiempo peleando con @keyframes y transiciones CSS. Las animaciones interactivas — efectos hover, modales, transiciones de página — se volvían especialmente caóticas al mezclarlas con el manejo de estado de React. Motion cambió eso por completo. Es declarativo, funciona de forma natural con el estado de React y gestiona las partes complicadas (como animar componentes al desmontarse) automáticamente. Este artículo cubre patrones prácticos desde fade-ins básicos hasta efectos activados por scroll y animaciones de layout.
Conceptos Fundamentales
Motion se centra en el componente motion. Añade el prefijo motion. a cualquier elemento HTML y obtendrá capacidades de animación:
import { motion } from "motion/react";
function FadeInBox() {
return (
<motion.div
initial={{ opacity: 0 }} // Starting state
animate={{ opacity: 1 }} // Target state
transition={{ duration: 0.5 }} // Animation settings
>
This fades in on mount
</motion.div>
);
}
Tres props que debes aprender:
initial— estado antes de que comience la animaciónanimate— estado objetivo al que se animatransition— duración, easing, delay, etc.
Propiedades de Animación Comunes
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
duration: 0.4,
ease: "easeOut",
delay: 0.1,
}}
>
Content
</motion.div>
Casi cualquier propiedad CSS funciona, pero prefiere opacity y propiedades de transform (x, y, scale, rotate). Están aceleradas por GPU y no provocan reflow del layout — manteniendo tus animaciones fluidas.
Patrones de Fade y Slide
Empezamos con los patrones más comunes.
Fade-in al Cargar la Página
"use client";
import { motion } from "motion/react";
function HeroSection() {
return (
<section>
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
Page Title
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.15 }}
>
Subtext here
</motion.p>
<motion.button
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.3 }}
>
Get Started
</motion.button>
</section>
);
}
Incrementar el delay crea un efecto stagger — los elementos aparecen secuencialmente.
Animaciones Hover
"use client";
import { motion } from "motion/react";
function AnimatedCard({ title, description }: { title: string; description: string }) {
return (
<motion.div
className="rounded-lg border p-6 cursor-pointer"
whileHover={{
scale: 1.02,
boxShadow: "0 10px 40px rgba(0,0,0,0.15)",
}}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
<h3>{title}</h3>
<p>{description}</p>
</motion.div>
);
}
whileHover y whileTap son los equivalentes declarativos de CSS :hover y :active.
Variants para Animaciones Complejas
Cuando varios elementos se animan juntos, usa variants — estados de animación nombrados que se pueden compartir entre padre e hijos.
"use client";
import { motion } from "motion/react";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Stagger children by 0.1s
delayChildren: 0.2, // Wait 0.2s before starting children
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" },
},
};
function AnimatedList({ items }: { items: string[] }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item, i) => (
<motion.li key={i} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}
staggerChildren hace el trabajo pesado aquí. Cuando el contenedor transiciona a "visible", cada hijo inicia su propia animación con un desfase de delay. No necesitas gestionar delays manualmente — solo pasa los mismos variants a cada motion.li.
Animaciones Activadas por Scroll (whileInView)
whileInView activa una animación cuando el elemento entra en el viewport:
"use client";
import { motion } from "motion/react";
function FeatureCard({ icon, title, description }: FeatureCardProps) {
return (
<motion.div
className="rounded-xl p-8 border"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} // Trigger 50px before fully in view
transition={{ duration: 0.5, ease: "easeOut" }}
>
<div className="text-4xl mb-4">{icon}</div>
<h3 className="text-xl font-bold mb-2">{title}</h3>
<p>{description}</p>
</motion.div>
);
}
Opciones de viewport:
once: true— anima solo la primera vez que entra en la vista (recomendado para revelaciones de contenido)margin— desplazamiento desde el borde del viewport; los valores negativos disparan antes de ser completamente visible
AnimatePresence: Animar Montaje y Desmontaje
Por defecto, React elimina componentes del DOM instantáneamente. AnimatePresence los mantiene en el DOM hasta que su animación de salida se completa.
"use client";
import { motion, AnimatePresence } from "motion/react";
function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
key="overlay"
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
key="modal"
className="fixed inset-x-4 top-1/2 -translate-y-1/2 max-w-lg mx-auto
bg-white rounded-2xl p-8 shadow-2xl z-10"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", stiffness: 350, damping: 30 }}
>
{children}
<button onClick={onClose}>Close</button>
</motion.div>
</>
)}
</AnimatePresence>
);
}
La prop exit define la animación que se reproduce antes de que el componente sea eliminado. Sin AnimatePresence, exit se ignora.
Notificaciones Toast
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
type Toast = { id: number; message: string };
function ToastContainer() {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (message: string) => {
const id = Date.now();
setToasts(prev => [...prev, { id, message }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
};
return (
<>
<button onClick={() => addToast("Saved!")}>Save</button>
<div className="fixed bottom-4 right-4 flex flex-col gap-2">
<AnimatePresence>
{toasts.map(toast => (
<motion.div
key={toast.id}
className="bg-gray-900 text-white px-4 py-3 rounded-lg shadow-lg"
initial={{ opacity: 0, x: 50, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 50, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{toast.message}
</motion.div>
))}
</AnimatePresence>
</div>
</>
);
}
useAnimate para Secuencias Imperativas
Para animaciones secuenciales complejas o animaciones activadas en momentos específicos, useAnimate te da control imperativo:
"use client";
import { useAnimate } from "motion/react";
function SubmitButton() {
const [scope, animate] = useAnimate();
const handleSubmit = async () => {
// 1. Press feedback
await animate(scope.current, { scale: 0.95 }, { duration: 0.1 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
// 2. Loading state
animate(scope.current, { opacity: 0.7 });
try {
await submitForm();
// 3. Success feedback
await animate(scope.current, { backgroundColor: "#22c55e" }, { duration: 0.3 });
await animate(scope.current, { backgroundColor: "#3b82f6" }, { duration: 0.5 });
} catch {
// 4. Error shake
await animate(scope.current, { x: [-8, 8, -8, 8, 0] }, { duration: 0.4 });
}
};
return (
<button ref={scope} onClick={handleSubmit} className="px-6 py-3 bg-blue-500 rounded-lg">
Submit
</button>
);
}
animate devuelve una Promise, así que puedes usar await en cada paso y encadenarlos secuencialmente.
Animaciones de Layout con layoutId
layoutId anima suavemente un elemento de una posición a otra — incluso entre diferentes renderizados de componentes:
"use client";
import { motion, LayoutGroup } from "motion/react";
import { useState } from "react";
function TabNav() {
const [activeTab, setActiveTab] = useState("home");
const tabs = ["home", "about", "contact"];
return (
<LayoutGroup>
<nav className="flex gap-4 relative">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className="relative px-4 py-2"
>
{tab}
{activeTab === tab && (
<motion.div
layoutId="active-indicator"
className="absolute inset-0 bg-blue-100 rounded-md -z-10"
/>
)}
</button>
))}
</nav>
</LayoutGroup>
);
}
Cuando active-indicator se mueve a una pestaña diferente, Motion automáticamente lo anima desde la posición anterior a la nueva. No necesitas rastrear coordenadas manualmente.
Buenas Prácticas de Rendimiento
Limítate a Transform y Opacity
// ❌ Causes layout reflow — expensive
<motion.div animate={{ width: "100%", height: 200, top: 50 }} />
// ✅ GPU-accelerated — no layout reflow
<motion.div animate={{ scaleX: 1, scaleY: 1, y: 50, opacity: 1 }} />
Cuando animas propiedades como width, height o top, el navegador recalcula el layout en cada frame. Limitarte a transform y opacity mantiene las animaciones en la GPU y fuera del hilo principal.
Movimiento Reducido para Accesibilidad
"use client";
import { motion, useReducedMotion } from "motion/react";
function AnimatedCard({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.4 }}
>
{children}
</motion.div>
);
}
useReducedMotion respeta la preferencia del usuario a nivel del sistema operativo para "Reducir movimiento" (la media query prefers-reduced-motion). Siempre tenlo en cuenta para aplicaciones en producción.
FAQ
¿Cuál es la diferencia entre Framer Motion y Motion?
Motion es el nuevo nombre de Framer Motion. En 2025, la librería se convirtió en un proyecto independiente bajo el paquete motion en npm. La API es idéntica — solo necesitas cambiar tu import de "framer-motion" a "motion/react". El paquete antiguo framer-motion aún se puede instalar pero ya no se desarrolla activamente.
¿Necesito "use client" en cada componente animado en Next.js?
Sí. Cualquier archivo que importe desde motion/react y use componentes motion necesita la directiva "use client" en Next.js App Router. La mejor práctica es mantener tus componentes a nivel de página como Server Components y extraer solo las secciones animadas en pequeños Client Components.
¿Cómo animo un componente cuando se desmonta?
Envuélvelo con AnimatePresence y añade una prop exit. Sin AnimatePresence, React elimina el componente instantáneamente del DOM y exit se ignora.
¿Motion es pesado? ¿Cuánto añade a mi bundle?
El core de Motion pesa alrededor de 32 KB gzipped (tree-shakeable). Si solo usas animaciones básicas (motion.div con initial/animate), el código real enviado es menor gracias al tree-shaking. Como comparación, una librería de animación CSS completa como GSAP tiene un tamaño similar pero no se integra con el estado de React.
¿Puedo usar Motion directamente con React Server Components?
No. Los componentes de Motion son del lado del cliente por naturaleza — requieren APIs del navegador y estado de React. Úsalos dentro de componentes "use client". Los Server Components manejan la obtención de datos y el renderizado estático; los Client Components manejan la interactividad y la animación.
¿Cuál es la mejor forma de escalonar una lista de elementos?
Usa variants con staggerChildren en el contenedor padre. Es más mantenible que configurar delay manualmente en cada elemento. Un valor de staggerChildren de 0.05–0.1 segundos se siente natural para la mayoría de las listas.
¿Cómo manejo prefers-reduced-motion?
Usa el hook useReducedMotion. Devuelve true cuando el usuario ha activado "Reducir movimiento" en la configuración de su sistema operativo. Configura duration: 0 o salta las animaciones de transform — mantén los cambios de opacidad para la visibilidad del contenido.
Conclusión
Patrones principales de Motion en una tabla:
| Patrón | Caso de Uso | Props Clave |
|---|---|---|
| Animación básica | Fade-in, slide | initial, animate, transition |
| Hover / tap | Feedback interactivo | whileHover, whileTap |
| Variants | Stagger en listas, animaciones coordinadas | variants, staggerChildren |
| Activado por scroll | Revelación de contenido | whileInView, viewport |
| Montaje / desmontaje | Modales, toasts | AnimatePresence, exit |
| Control secuencial | Coreografía compleja | useAnimate |
| Cambios de layout | Pestañas, reordenamiento de listas | layoutId, LayoutGroup |
La regla más importante: no sobre-animes. opacity + y cubre el 80% de los casos de forma elegante. Empieza simple, recurre a patrones complejos solo cuando sea necesario, y siempre mantén transform/opacity como tus propiedades principales de animación para el rendimiento. Consulta la documentación de Motion y el changelog para las últimas novedades.
Artículos relacionados: