< Go back

Mastering Motion Design: When to Use GSAP vs CSS vs Framer Motion

12/6/2026 · 16 min read

Most developers pick their animation tool once and use it for everything. That's like using a sledgehammer for every nail. GSAP, CSS animations and Framer Motion each have specific strengths — and picking the wrong one costs you either performance, developer experience, or both.

This article gives you a decision framework. Not "GSAP is best" or "just use CSS" — but a clear understanding of what each tool actually does, where it excels, and the specific situations where you should reach for each one. I've built extensively with all three, and the answer is almost never one-size-fits-all.

The Core Trade-offs at a Glance
  • CSS animations: Zero bundle cost, GPU-accelerated by default, limited control. Best for simple transitions and hover states.
  • GSAP: Maximum control and performance, ~30KB gzipped, framework-agnostic. Best for complex sequences, scroll-driven animations and anything timeline-based.
  • Framer Motion: React-native, declarative API, ~50KB gzipped. Best for React component animations, layout transitions and gesture-driven interactions.

CSS Animations: When to Use Them

CSS animations are the default choice — not because they're always right, but because they have zero runtime cost. If something can be done in CSS, it should be.

CSS Wins When:
  • The animation is simple and looping. Loading spinners, pulsing indicators, skeleton screens — anything that runs continuously on a fixed loop is pure CSS territory.
  • You're animating only transform and opacity. These two properties are compositor-layer only — the browser never triggers layout or paint. CSS animations on transform/opacity are as performant as it gets.
  • Hover and focus states. transition: transform 200ms ease-out on a button is simpler, more maintainable, and performs identically to a JavaScript equivalent.
  • No JavaScript dependency is available. SSR content that renders before JS loads, CSS-only components in email or print contexts.
/* ✅ CSS: right tool for the job */
.card {
  transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1),
              box-shadow 200ms ease;
}
.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}

/* Scroll-driven animation (CSS-native, 2024+) */
@keyframes fade-in-up {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}
.reveal {
  animation: fade-in-up 0.6s ease both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
CSS Loses When:
  • You need to sequence multiple elements with precise timing relationships
  • The animation needs to respond to runtime data (scroll position, mouse position, user input)
  • You need to pause, reverse, or scrub through an animation
  • You're animating more than 3-4 properties and the CSS becomes unmaintainable

GSAP: The Professional's Toolkit

GSAP (GreenSock Animation Platform) is the industry standard for complex web animation. It's been around since 2008, runs on everything from WordPress to React to vanilla HTML, and has performance characteristics that CSS simply can't match for complex sequences.

GSAP Wins When:
  • Scroll-driven storytelling. ScrollTrigger is the best scroll animation tool that exists. Scrubbing a timeline to scroll position, pinning sections, creating parallax at precise offsets — nothing comes close.
  • Complex sequences. A 12-step entrance animation where elements stagger with precise timing, easing curves and callbacks requires a timeline. GSAP timelines are readable, debuggable and reversible.
  • Framework-agnostic projects. Plain HTML/CSS sites, WordPress, Webflow, Shopify — anywhere you can't or won't use React, GSAP is your only real option for serious animation.
  • SVG and canvas animation. GSAP handles SVG morphing, drawing, and motion paths natively. CSS can't touch SVG attributes; Framer Motion doesn't support canvas.
  • Physics and advanced easing. GSAP's CustomEase, Elastic, and Bounce eases produce motion that looks genuinely physical without a physics engine.
// ✅ GSAP: right tool for scroll-driven sequences
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: '.hero',
    start: 'top top',
    end: 'bottom top',
    scrub: 1,
    pin: true
  }
});

tl.to('.hero-title', { y: -100, opacity: 0, duration: 0.4 })
  .to('.hero-image', { scale: 1.2, duration: 0.8 }, '<')
  .from('.next-section', { y: 60, opacity: 0, duration: 0.4 });
GSAP Loses When:
  • You're in a React codebase and your animations are tied to component state and layout changes — GSAP has no awareness of the React render cycle
  • You need gesture-driven interactions (drag, pan, swipe) — this is Framer Motion's native territory
  • Bundle size is critical and the animation is simple enough for CSS

Framer Motion: React-Native Animation

Framer Motion is the right choice specifically when you're building React applications and your animations are tied to component lifecycle, state changes, or layout shifts.

Framer Motion Wins When:
  • List reordering and layout animations. layout prop + AnimatePresence handles animated list additions, removals, and reorders with almost zero code. Doing this in GSAP requires significant manual work.
  • Shared element transitions. Animating an element from one position to another across route changes (expanding card → detail page) is trivially easy with layoutId. It's genuinely hard with any other tool.
  • Gesture-driven interactions. Drag, pan, and swipe with spring physics, velocity-based release, and snap points — Framer Motion handles this declaratively. GSAP's Draggable is powerful but verbose by comparison.
  • State-driven animations in React. When your animation is directly tied to component state (isOpen, isSelected), Framer Motion's declarative API is far cleaner than imperative GSAP.
// ✅ Framer Motion: right tool for React layout animations
import { motion, AnimatePresence } from 'framer-motion';

function NotificationList({ notifications }) {
  return (
    
    {notifications.map(n => ( {n.message} ))}
); }
Framer Motion Loses When:
  • You're not using React — Framer Motion is React-only
  • You need precise timeline control or scroll-scrubbing — ScrollTrigger is significantly more powerful
  • Bundle size is critical — at ~50KB gzipped it's the heaviest of the three options
  • You're doing SVG animation or canvas work

The Decision Framework

  • Is it a simple hover/transition on transform or opacity? → CSS. No question.
  • Is it a continuous loop (spinner, pulse, skeleton)? → CSS @keyframes.
  • Is it scroll-driven, timeline-sequenced, or SVG-based? → GSAP.
  • Is it in a React app, tied to component state or layout changes? → Framer Motion.
  • Is it a gesture interaction (drag, swipe) in React? → Framer Motion.
  • Is it a complex entrance sequence on a marketing site without React? → GSAP.
  • Is it a shared element transition between routes in Next.js? → Framer Motion with layoutId.
Can You Use GSAP and Framer Motion Together?

Yes — and it's often the right answer in React apps. Use Framer Motion for component-level animations (enters, exits, layout shifts) and GSAP + ScrollTrigger for the marketing page hero section. They don't conflict. The React component tree doesn't need to know about GSAP, and GSAP doesn't need to know about React's render cycle as long as you're careful with refs and cleanup in useEffect.

// GSAP inside React: safe pattern
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

function HeroSection() {
  const sectionRef = useRef(null);

  useEffect(() => {
    const ctx = gsap.context(() => {
      gsap.from('.hero-word', {
        y: 80, opacity: 0, duration: 0.8,
        stagger: 0.08,
        ease: 'power3.out',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top 80%'
        }
      });
    }, sectionRef);

    return () => ctx.revert(); // ✅ cleanup prevents memory leaks
  }, []);

  return 
...
; }

Performance: What Actually Matters

All three tools can produce janky animations if used wrong. The rules are the same regardless of tool:

  • Only animate transform and opacity. These run on the compositor thread — no layout, no paint. Animating width, height, top, left or margin forces layout recalculation every frame and will drop to below 60fps on mid-range devices.
  • Use will-change sparingly. It promotes elements to their own compositor layer, which helps performance but costs memory. Only apply it to elements that are actively animating.
  • Reduce the number of animating elements. Animating 200 list items simultaneously is a performance problem regardless of tool. Stagger aggressively and consider CSS content-visibility for off-screen elements.
  • Respect prefers-reduced-motion. This is non-negotiable. All three tools have built-in ways to respect it — use them.
/* CSS: reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

// GSAP: reduced motion
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  gsap.from('.hero', { y: 60, opacity: 0, duration: 0.8 });
}

// Framer Motion: built-in
const { prefersReducedMotion } = useReducedMotion();
// or use the shouldReduceMotion utility

Conclusion

CSS animations for anything simple. GSAP for scroll-driven sequences, complex timelines, SVG, and non-React projects. Framer Motion for React component animations, layout transitions, and gesture interactions. Use two or all three in the same project when the use case calls for it — they're tools, not religions. The goal is an experience that feels considered and performant, not a codebase that uses only one animation library.
Next article

Mastering Component Architecture: Design Systems That Don't Break →

Would you like to collaborate?

Contact me