32blogby Studio Mitsu

Motion para React: Guía de Animaciones (antes Framer Motion)

Desde fade-ins hasta efectos por scroll y transiciones de página — patrones prácticos de Motion (antes Framer Motion) con ejemplos funcionales para React.

by omitsu11 min read
ReactFramer MotionanimationCSSUX
Contenido

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:

tsx
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ón
  • animate — estado objetivo al que se anima
  • transition — duración, easing, delay, etc.

Propiedades de Animación Comunes

tsx
<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

tsx
"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

tsx
"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.

tsx
"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:

tsx
"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.

tsx
"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

tsx
"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:

tsx
"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:

tsx
"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

tsx
// ❌ 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

tsx
"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ónCaso de UsoProps Clave
Animación básicaFade-in, slideinitial, animate, transition
Hover / tapFeedback interactivowhileHover, whileTap
VariantsStagger en listas, animaciones coordinadasvariants, staggerChildren
Activado por scrollRevelación de contenidowhileInView, viewport
Montaje / desmontajeModales, toastsAnimatePresence, exit
Control secuencialCoreografía complejauseAnimate
Cambios de layoutPestañas, reordenamiento de listaslayoutId, 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: