DEV Community

Cover image for Motion vs GSAP in React: Which Animation Library Should You Choose?
Lucy Muturi for Syncfusion, Inc.

Posted on Originally published at syncfusion.com on

Motion vs GSAP in React: Which Animation Library Should You Choose?

TL;DR: Choosing between Motion and GSAP isn’t about which library is better, but which one fits your project. This comparison explores how both libraries handle React animations, page transitions, exit animations, performance, timelines, and developer experience. Learn where Motion’s React-first approach simplifies UI animation workflows and where GSAP’s powerful timeline engine and ScrollTrigger capabilities make it the stronger option for complex interactive experiences.

Building animations in React is not just about making elements move. It is about making motion work with component state, mounting and unmounting, route changes, and interaction patterns that are common in modern apps.

That is why React developers often compare Motion and GSAP. Both can produce polished animations. The bigger difference is how each library fits React’s rendering model and the kind of work your project needs to do.

If your animations are mostly tied to component state, enter and exit transitions, gestures, and reusable UI patterns, Motion usually feels more natural. If your project depends on tightly choreographed sequences, advanced scroll effects, or animation logic that goes beyond React components, GSAP often gives you more control.

In this article, we will compare Motion and GSAP in React across three common scenarios:

  • Exit animations
  • Hover and tap interactions
  • Page transitions

We will also look at accessibility, performance, and the kinds of projects where each library makes the most sense.

Motion and GSAP at a glance

Before diving into code, here is the short version.

Use case Motion GSAP
React component animation Excellent Good
Declarative animation Strong Limited
Exit animations Excellent Requires more orchestration
Hover and tap interactions Built in Manual handling
Timelines Good Excellent
Scroll-driven animation Good Excellent
SVG-heavy animation Good Excellent
Framework independence React-focused Excellent
Complex sequencing Good Excellent
Learning curve for React teams Lower Higher

Note: Motion package naming

Motion is the current package name for the library previously known as Framer Motion. The recommended package is now motion, with React imports from motion/react. The older framer-motion package still exists for compatibility, but the examples in this article use motion/react for consistency.

Why animation feels different in React

Before React, most frontend animation was done imperatively. You selected an element, changed styles, and told the browser exactly what to do.

JSX

// 1. Select the element
const box = document.getElementById('myBox');

// 2. Imperatively change styles
box.style.backgroundColor = 'blue';
box.style.width = '200px';
box.style.padding = '15px';
Enter fullscreen mode Exit fullscreen mode

That model works well when you are directly controlling the DOM.

React changes the mental model. Instead of manually changing DOM nodes, you describe the UI as a function of state and props. React decides when to update the DOM.

JSX

import React, { useState } from 'react';

export default function App() {
  const [isActive, setIsActive] = useState(false);

  return (
    <button onClick={() => setIsActive(!isActive)}>
      {isActive ? 'Active' : 'Inactive'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

That difference matters for animation.

In React, the hard part is not whether an element can animate. The hard part is coordinating animation with component lifecycle. If a component unmounts, React can remove its DOM node immediately. If you want an exit animation to finish first, something has to keep that node around long enough for the animation to complete.

The same kind of coordination problem appears when React reuses a DOM node or rerenders a subtree while an animation is still in progress. This is where Motion and GSAP take different approaches.

Motion: built for React components

Motion was designed to work directly with React’s component model. Instead of selecting DOM nodes and animating them manually, you declare animation behavior on components.

JSX

import { motion } from 'motion/react';

< motion.div
  initial={{ opacity: 0, y: 30 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.4, ease: "easeOut" }}
>
  Dashboard Panel
</motion.div>
Enter fullscreen mode Exit fullscreen mode

This reads like ordinary React because the animation lives alongside the component definition.

For the common React use case, state changes drive UI changes, and animations follow from those changes, Motion removes a lot of manual work. You usually don’t need to wire refs, effects, and cleanup logic just to animate a component into view.

The biggest advantage appears when components leave the screen. React normally removes a component as soon as it is no longer rendered. Motion’s AnimatePresence lets exit animations finish before the DOM node is removed.

JSX

import { motion, AnimatePresence } from 'motion/react';

<AnimatePresence>
  {show && (
    <motion.div
      key="modal"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  )}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

Motion also provides built-in patterns for common UI interactions.

  • Hover states
  • Tap interactions
  • Drag gestures
  • Page transitions

That makes it especially comfortable in product UIs, admin panels, dashboards, and design systems where motion is tied closely to component behavior.

Another strength is variants. Variants let parent and child animations coordinate naturally through the component tree.

JSX

// Staggered card list -- each card staggers in 80ms after the previous one
import { motion } from 'motion/react';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.08,
    },
  },
};

const cardVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
};

export function IssueList({ issues }) {
  return (
    <motion.ul variants={containerVariants} initial="hidden" animate="visible">
      {issues.map((issue) => (
        <motion.li key={issue.id} variants={cardVariants}>
          <IssueCard issue={issue} />
        </motion.li>
      ))}
    </motion.ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

The parent controls the stagger. Each child only defines what its own states mean. That pattern scales well for state-driven UI animation.

GSAP: animation-first, DOM-oriented, extremely flexible

GSAP approaches animation from the opposite direction. It works directly with DOM elements and gives you precise control over timing, sequencing, and complex motion.

In React, that usually means using refs and lifecycle-aware hooks.

JSX

import { useRef, useEffect } from 'react';
import gsap from 'gsap';

function Box() {
  const boxRef = useRef(null);

  useEffect(() => {
    const ctx = gsap.context(() => {
      gsap.from(boxRef.current, { opacity: 0, y: 20, duration: 0.4 });
    }, boxRef);

    return () => ctx.revert(); // cleanup on unmount
  }, []);

  return <div ref={boxRef} className="box" />;
}
Enter fullscreen mode Exit fullscreen mode

GSAP does not depend on React. That is one of its biggest strengths. It works across frameworks and gives you a consistent animation model even when your UI is not purely component-driven.

For React projects, GSAP now provides @gsap/react, which includes a useGSAP() hook designed to make integration cleaner. It helps scope and clean up GSAP objects created during the hook run.

Where GSAP stands out most is complex sequencing. If you need a highly choreographed animation involving multiple elements with precise overlaps and offsets, GSAP timelines are one of the strongest tools available in frontend animation.

JSX

useEffect(() => {
  const ctx = gsap.context(() => {
    const tl = gsap.timeline();
    tl.from('.hero-title', { opacity: 0, y: 40, duration: 0.6 })
      .from('.hero-subtitle', { opacity: 0, y: 20, duration: 0.4 }, '-=0.2')
      .from('.hero-cta', { opacity: 0, scale: 0.9, duration: 0.3 }, '-=0.1');
  });

  return () => ctx.revert();
}, []);
Enter fullscreen mode Exit fullscreen mode

Motion can handle sequencing, but GSAP is usually the better fit for hand-crafted, timeline-heavy motion, advanced scroll effects, and animation outside standard React component patterns.

React animation examples in Motion and GSAP

To make the differences concrete, here are a few common React animation scenarios implemented in both libraries.

Example 1: Exit animation before unmount

Exit animations are where React animation gets more interesting. The challenge is that React removes the DOM node as soon as the component stops rendering, unless something intercepts that process.

Motion

ExitAnimationMotion.jsx

import { motion, AnimatePresence } from 'motion/react';

export function ToastNotification({ show, message }) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          key="toast"
          className="toast"
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          transition={{ duration: 0.3 }}
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is a strong example of Motion’s React-first design. AnimatePresence keeps the element mounted long enough for the exit animation to finish.

GSAP:

This example uses the @gsap/react useGSAP() hook, which is the recommended pattern for React integrations.

ExitAnimationGSAP.jsx

// production-ready
import { useRef, useState, useEffect } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

gsap.registerPlugin(useGSAP);

export function ToastNotification({ show, message }) {
  const [shouldRender, setShouldRender] = useState(show);
  const toastRef = useRef(null);

  useEffect(() => {
    if (show) setShouldRender(true);
  }, [show]);

  useGSAP(() => {
    if (!shouldRender || !toastRef.current) return;

    if (show) {
      gsap.fromTo(toastRef.current,
        { opacity: 0, y: -20 },
        { opacity: 1, y: 0, duration: 0.3 }
      );
    } else {
      gsap.to(toastRef.current, {
        opacity: 0,
        y: -20,
        duration: 0.3,
        onComplete: () => setShouldRender(false),
      });
    }
  }, {
    dependencies: [show, shouldRender],
    scope: toastRef,
    // Kills the previous run's tween before this run starts
    //This is what makes rapid show/hide toggling safe.
    revertOnUpdate: true,
  });

  if (!shouldRender) return null;

  return (
    <div ref={toastRef} className="toast">
      {message}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This implementation is workable, but it shows the extra coordination GSAP often needs in React. You have to keep the component mounted after show becomes false, wait for the exit animation to complete, then remove it.

If your app uses many modals, drawers, tooltips, and toasts, this difference adds up quickly.

Best fit in this scenario: Motion

Example 2: Hover and Tap Interactions

Buttons, cards, and interactive controls often need hover and press feedback.

Motion

InteractiveButtonMotion.jsx

import { motion } from 'motion/react';

export function AnimatedButton({ children, onClick }) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
      className="btn-primary"
    >
      {children}
    </motion.button>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is concise and expressive. Motion’s gesture props fit UI-level interactions well.

GSAP:

This example uses Pointer Events, contextSafe(), and explicit overwrite handling with @gsap/react.

InteractiveButtonGSAP.jsx

// production-ready
import { useRef } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

gsap.registerPlugin(useGSAP);

export function AnimatedButton({ children, onClick }) {
  const btnRef = useRef(null);

  // contextSafe() marks handler-triggered tweens as trackable/cleanable,

  // same as tweens created directly inside useGSAP().
  const { contextSafe } = useGSAP({ scope: btnRef });

  const animate = contextSafe((vars) => {
    gsap.to(btnRef.current, { ...vars, overwrite: 'auto' });
  });

  const handlePointerEnter = () => animate({ scale: 1.05, duration: 0.2, ease: 'power2.out' });
  const handlePointerLeave = () => animate({ scale: 1, duration: 0.2, ease: 'power2.out' });
  const handlePointerDown = () => animate({ scale: 0.95, duration: 0.1, ease: 'power2.in' });
  const handlePointerUp = () => animate({ scale: 1.05, duration: 0.1, ease: 'power2.out' });

  return (
    <button
      ref={btnRef}
      onClick={onClick}
      onPointerEnter={handlePointerEnter}
      onPointerLeave={handlePointerLeave}
      onPointerDown={handlePointerDown}
      onPointerUp={handlePointerUp}
      onPointerCancel={handlePointerLeave}
      className="btn-primary"
    >
      {children}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

GSAP can absolutely handle this, but the interaction logic is more manual. That is not a flaw in GSAP so much as a sign that it operates at a lower level. For common UI interaction states, Motion usually asks for less code.

Best fit in this scenario: Motion

Read the full blog post on the Syncfusion Website

Top comments (0)