Web Development

Web Animation with GSAP and Framer Motion: A Developer Guide to Performant Animations

Web Animation with GSAP and Framer Motion: A Developer Guide to Performant Animations

Animation transforms static interfaces into living, breathing experiences. When a button subtly pulses to invite interaction, when page elements gracefully slide into view during scrolling, or when data visualizations unfold with cinematic flair — these moments define the difference between software that merely functions and software that genuinely delights. But animation on the web has always walked a tightrope between visual ambition and runtime performance.

Two libraries have emerged as the dominant forces in web animation: GSAP (GreenSock Animation Platform) and Framer Motion. Each brings a distinct philosophy to the challenge of making things move smoothly in a browser. GSAP, the battle-tested veteran, offers unmatched control and cross-framework compatibility. Framer Motion, the React-native newcomer, provides a declarative API that feels like writing animation as naturally as writing JSX. Understanding both — their strengths, their trade-offs, and when to reach for each — is essential knowledge for any frontend developer building modern interfaces.

The Performance Foundation: Why Animation Choice Matters

Before diving into library specifics, it is worth understanding what makes web animation performant or sluggish. Browsers render pages through a pipeline: style calculation, layout, paint, and composite. Animations that trigger layout recalculations (changing width, height, margin, or padding) force the browser to reflow the entire document. Paint-triggering properties like background-color or box-shadow are less expensive but still costly. The gold standard is compositor-only properties — transforms and opacity — which the GPU handles independently without touching the main thread.

Both GSAP and Framer Motion are built around this principle, but they approach it differently. GSAP manipulates inline styles directly and offers fine-grained control over exactly which properties animate. Framer Motion abstracts this behind a declarative layer, automatically optimizing for transform-based animations when possible. The choice between them often depends less on raw performance and more on your project’s framework, team expertise, and the complexity of animations you need to orchestrate. If your project involves server-side or client-side rendering decisions, the animation library you pick can influence your architectural approach significantly.

GSAP: The Swiss Army Knife of Web Animation

GreenSock has been a staple of web animation since the Flash era. Its JavaScript incarnation, GSAP, carries forward decades of animation engineering into a library that works with vanilla JS, React, Vue, Angular, Svelte, or any other framework. The core library weighs roughly 25KB minified and gzipped, with additional plugins available for specialized behaviors like scroll-triggered animations, morphing SVG paths, and physics-based motion.

Core Concepts and API Design

GSAP’s API revolves around tweens and timelines. A tween is a single animation — moving an element from point A to point B over a specified duration. A timeline sequences multiple tweens together, providing precise control over when each animation starts, how they overlap, and how the entire sequence behaves as a unit.

The three fundamental tween methods are gsap.to() (animate to a target state), gsap.from() (animate from a starting state to the current state), and gsap.fromTo() (explicitly define both start and end states). Each accepts a target (CSS selector, DOM element, or object), a duration, and a vars object containing the properties to animate along with easing functions, delays, callbacks, and other configuration.

GSAP’s easing system deserves special mention. Beyond standard CSS easing curves, GSAP provides dozens of built-in eases — bounce, elastic, stepped, rough, slow-mo — plus a visual ease editor. Custom easing is where GSAP truly shines: you can define eases as cubic bezier curves, spring physics, or even completely custom functions. This level of control is invaluable when matching animations to a brand’s motion design language or replicating motion design specs from tools like After Effects.

ScrollTrigger: Where GSAP Dominates

GSAP’s ScrollTrigger plugin is arguably its most compelling feature. It connects any GSAP animation to scroll position, enabling scroll-driven animations, pinning sections, snapping to positions, and creating parallax effects with minimal code. Unlike Intersection Observer-based solutions that simply detect visibility, ScrollTrigger maps scroll progress to animation progress, enabling smooth scrubbing effects where the animation literally follows the user’s scroll position.

For projects that rely on scroll-based storytelling — product landing pages, interactive reports, portfolio sites — ScrollTrigger is unmatched. It handles edge cases that trip up simpler solutions: reverse scrolling, mobile overscroll bounce, resize events, and dynamic content height changes. Combined with GSAP’s timeline system, you can orchestrate entire scroll-driven narratives with frame-perfect precision. This pairs well with understanding modern CSS layout techniques that establish the structural foundation your animations will enhance.

Practical GSAP Example: Staggered Card Reveal with ScrollTrigger

Here is a complete, production-ready example that reveals a grid of cards as the user scrolls. Each card fades in and slides up with a staggered delay, creating a cascading waterfall effect:

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

function initCardReveal() {
  const cards = gsap.utils.toArray(".feature-card");

  // Set initial state — cards are invisible and shifted down
  gsap.set(cards, {
    opacity: 0,
    y: 60,
    scale: 0.95,
  });

  // Create a timeline triggered by scroll position
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: ".features-grid",
      start: "top 80%",    // animation begins when grid top hits 80% of viewport
      end: "bottom 20%",   // animation ends when grid bottom hits 20% of viewport
      toggleActions: "play none none reverse", // play on enter, reverse on leave
    },
  });

  // Stagger each card's entrance with overlapping timing
  tl.to(cards, {
    opacity: 1,
    y: 0,
    scale: 1,
    duration: 0.8,
    stagger: {
      amount: 0.6,        // total stagger spread across all cards
      from: "start",      // stagger from the first card
      ease: "power2.out",
    },
    ease: "power3.out",
  });

  // Add a subtle hover effect to each card
  cards.forEach((card) => {
    card.addEventListener("mouseenter", () => {
      gsap.to(card, {
        y: -8,
        boxShadow: "0 20px 40px rgba(0, 0, 0, 0.15)",
        duration: 0.3,
        ease: "power2.out",
      });
    });

    card.addEventListener("mouseleave", () => {
      gsap.to(card, {
        y: 0,
        boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
        duration: 0.3,
        ease: "power2.inOut",
      });
    });
  });
}

// Initialize when DOM is ready
document.addEventListener("DOMContentLoaded", initCardReveal);

This example demonstrates several GSAP best practices. Setting initial state with gsap.set() avoids a flash of unstyled content. The toggleActions parameter ensures smooth behavior when scrolling both directions. The stagger configuration creates visual rhythm without manual delay calculations. And the hover effects use separate tweens that can interrupt each other naturally, a pattern GSAP handles with its built-in overwrite logic.

Framer Motion: Declarative Animation for React

Framer Motion takes a fundamentally different approach. Rather than imperatively telling elements how to move, you declare what states they should be in, and the library handles transitions between those states. Born from the Framer prototyping tool, it is deeply integrated with React’s component model — animations are expressed as props on special motion components rather than as imperative function calls.

The Motion Component Paradigm

Every HTML and SVG element has a corresponding motion counterpart. A <motion.div> behaves exactly like a <div> but accepts animation props: initial (starting state), animate (target state), exit (leaving state), transition (timing configuration), and whileHover, whileTap, whileInView, and whileDrag for interactive states.

This declarative model meshes perfectly with React’s philosophy. Animation state lives alongside component state. Conditional rendering triggers enter and exit animations automatically through the AnimatePresence component. And because Framer Motion handles its own rendering through the WAAPI (Web Animations API) and requestAnimationFrame, it can animate layout changes that would normally require expensive layout recalculations.

Layout Animations: Framer Motion’s Secret Weapon

Perhaps Framer Motion’s most impressive feature is its layout prop. Adding layout to a motion component tells Framer Motion to automatically animate any changes to that element’s size or position in the document flow. Reorder a list, toggle between grid and list views, expand an accordion — the elements smoothly transition to their new positions without you writing a single line of animation logic.

This is technically extraordinary. Under the hood, Framer Motion measures the element’s position before and after the layout change, then uses FLIP (First, Last, Invert, Play) technique to animate the difference using only transforms. The result is buttery-smooth layout animations that run entirely on the compositor, even though the underlying CSS layout has changed dramatically. If you are building reusable UI components in Storybook, integrating Framer Motion’s layout animations can elevate your component library significantly.

Variants: Managing Complex Animation State

For components with multiple animation states — think a navigation menu that can be open, closed, or in a compact mobile mode — Framer Motion offers variants. Variants are named animation states defined as objects, and they propagate automatically through the component tree. A parent component switching to the “open” variant will cause all children with matching variant definitions to animate simultaneously, with optional stagger delays.

This propagation model eliminates the need for complex state management around animations. You define your animation vocabulary once, and React’s component hierarchy becomes your timeline. Combined with gesture handlers like whileHover and whileTap, variants enable sophisticated interactive animations with remarkably little code.

Practical Framer Motion Example: Animated Task List with Reordering

This example builds an animated task list where items can be added, removed, and toggled — all with smooth transitions. It demonstrates AnimatePresence, layout animations, and gesture-driven interactions:

import { useState } from "react";
import { motion, AnimatePresence, Reorder } from "framer-motion";

const taskVariants = {
  initial: { opacity: 0, x: -30, scale: 0.96 },
  animate: { opacity: 1, x: 0, scale: 1 },
  exit: { opacity: 0, x: 30, scale: 0.96, transition: { duration: 0.25 } },
};

function TaskItem({ task, onToggle, onDelete }) {
  return (
    <Reorder.Item
      value={task}
      variants={taskVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      layout
      whileHover={{ backgroundColor: "rgba(194, 114, 78, 0.06)" }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 30 }}
      style={{
        display: "flex",
        alignItems: "center",
        padding: "12px 16px",
        borderRadius: "8px",
        cursor: "grab",
      }}
    >
      <motion.button
        onClick={() => onToggle(task.id)}
        whileTap={{ scale: 0.85 }}
        style={{
          width: 22,
          height: 22,
          borderRadius: "50%",
          border: "2px solid #c2724e",
          backgroundColor: task.done ? "#c2724e" : "transparent",
          marginRight: 12,
        }}
        layout
      />

      <motion.span
        layout
        style={{
          flex: 1,
          textDecoration: task.done ? "line-through" : "none",
          color: task.done ? "#a8a29e" : "#1c1917",
        }}
        animate={{ opacity: task.done ? 0.5 : 1 }}
      >
        {task.text}
      </motion.span>

      <motion.button
        onClick={() => onDelete(task.id)}
        whileHover={{ scale: 1.15, color: "#dc2626" }}
        whileTap={{ scale: 0.9 }}
      >
        ✕
      </motion.button>
    </Reorder.Item>
  );
}

export default function AnimatedTaskList() {
  const [tasks, setTasks] = useState([
    { id: 1, text: "Review animation performance", done: false },
    { id: 2, text: "Implement scroll triggers", done: false },
    { id: 3, text: "Test on mobile devices", done: false },
  ]);

  const toggleTask = (id) =>
    setTasks((prev) =>
      prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
    );

  const deleteTask = (id) =>
    setTasks((prev) => prev.filter((t) => t.id !== id));

  const addTask = (text) =>
    setTasks((prev) => [...prev, { id: Date.now(), text, done: false }]);

  return (
    <div style={{ maxWidth: 480, margin: "0 auto" }}>
      <Reorder.Group axis="y" values={tasks} onReorder={setTasks}>
        <AnimatePresence mode="popLayout">
          {tasks.map((task) => (
            <TaskItem
              key={task.id}
              task={task}
              onToggle={toggleTask}
              onDelete={deleteTask}
            />
          ))}
        </AnimatePresence>
      </Reorder.Group>
    </div>
  );
}

Notice how the layout prop on multiple elements ensures that when a task is removed, remaining items smoothly slide into their new positions. The AnimatePresence wrapper with mode="popLayout" handles exit animations without disrupting the layout flow. Spring physics on the transitions create natural, organic movement that feels responsive rather than mechanical. For projects that manage complex state alongside animations, understanding robust error handling patterns will help you build resilient animated interfaces.

GSAP vs. Framer Motion: A Practical Comparison

Choosing between these libraries is not about which is “better” — it is about which is better for your specific context. Here is how they compare across dimensions that matter in production.

Framework Compatibility

GSAP works everywhere: vanilla JavaScript, React, Vue, Svelte, Angular, WordPress, static HTML. It is framework-agnostic by design. Framer Motion is React-only (though the underlying Motion library now supports vanilla JS as well). If your project uses Vue with the Composition API or another non-React framework, GSAP is the clear choice. For React projects, Framer Motion’s deep integration with React’s rendering model offers a more idiomatic developer experience.

Bundle Size

GSAP’s core is approximately 25KB gzipped. ScrollTrigger adds roughly 10KB. Framer Motion’s bundle is larger — around 32-40KB gzipped for typical usage, though tree-shaking can reduce this. For performance-critical applications, particularly those where SEO and JavaScript payload size intersect, GSAP’s modular architecture often results in a smaller total footprint.

Learning Curve

GSAP has a steeper initial learning curve but a more predictable scaling curve. Its imperative API requires you to understand animation concepts explicitly, but once learned, it applies uniformly across any complexity level. Framer Motion is easier to start with — simple animations require just a few props — but complex orchestrations can become harder to reason about as declarative abstractions sometimes hide the underlying behavior.

Performance Characteristics

Both libraries deliver excellent performance for typical use cases. GSAP has a slight edge in raw animation performance, particularly for complex sequences with many simultaneous tweens, because it bypasses React’s reconciliation entirely. Framer Motion’s layout animations, while technically impressive, can cause performance hiccups on lower-end devices when animating many elements simultaneously, as the FLIP technique requires measuring DOM positions.

For either library, the performance fundamentals remain the same: animate transforms and opacity whenever possible, avoid animating properties that trigger layout, use will-change sparingly, and profile on real devices. Understanding how web components encapsulate styles and behavior can also help you architect animation boundaries that perform well at scale.

When to Choose GSAP

  • Scroll-driven narratives — ScrollTrigger is best-in-class for mapping animations to scroll position
  • Cross-framework projects — GSAP works identically regardless of your UI framework
  • Complex timelines — orchestrating dozens of elements with precise timing is where GSAP’s timeline API excels
  • SVG animation — GSAP’s MorphSVG and DrawSVG plugins have no equivalent in Framer Motion
  • Non-React projects — Vue, Svelte, Angular, or vanilla JS projects benefit from GSAP’s framework-agnostic design
  • Pixel-perfect motion design — when matching exact After Effects or Principle specs, GSAP’s easing and timing controls are more precise

When to Choose Framer Motion

  • React applications — the declarative API integrates naturally with React’s mental model
  • Layout transitions — automatic FLIP-based layout animations are uniquely powerful
  • Component-level animation — enter/exit animations tied to React’s component lifecycle
  • Gesture-driven UI — drag, hover, and tap interactions are first-class citizens
  • Rapid prototyping — simple animations require minimal code
  • Design system animations — variants provide a clean vocabulary for animation states across a component library, especially when building with modern component libraries like shadcn/ui

Advanced Techniques for Both Libraries

Accessible Animations

Performant animation means nothing if it excludes users. Both libraries should respect the prefers-reduced-motion media query. In GSAP, you can check window.matchMedia("(prefers-reduced-motion: reduce)").matches and conditionally disable or simplify animations. Framer Motion provides a useReducedMotion hook that returns a boolean, making it trivial to swap elaborate animations for simple fades or instant transitions.

Beyond motion preferences, consider animation’s impact on cognitive load. Not every state change needs to be animated. Reserve motion for moments where it communicates meaning — showing spatial relationships, confirming actions, or guiding attention. Gratuitous animation is worse than no animation at all.

Performance Profiling

Chrome DevTools’ Performance panel is essential for validating animation performance. Record a session, then examine the frames-per-second chart and look for long tasks that might cause jank. The Layers panel shows which elements are promoted to their own compositor layers — you want animated elements on separate layers without over-promoting static content.

GSAP provides gsap.ticker for monitoring its internal frame rate, and the GSDevTools plugin offers a visual timeline scrubber for debugging complex sequences. Framer Motion’s animations can be profiled through React DevTools’ Profiler, which shows re-renders caused by animation state changes.

Combining Both Libraries

There is no rule against using both libraries in the same project. A practical pattern is using Framer Motion for component-level micro-interactions (hover states, mount/unmount transitions, layout shifts) and GSAP for macro-level orchestration (scroll-driven sequences, complex timelines, SVG morphing). The key is avoiding conflicts: do not animate the same property on the same element with both libraries simultaneously.

For teams building ambitious web experiences — marketing sites, interactive documentation, data visualization dashboards — this hybrid approach often delivers the best results. Project management becomes crucial in such scenarios, and tools like Taskee can help coordinate animation work across designers and developers, tracking which interactions need implementation and ensuring design specifications translate accurately to code.

The Broader Animation Ecosystem

GSAP and Framer Motion do not exist in isolation. CSS animations and transitions handle simple state changes efficiently with zero JavaScript overhead. The Web Animations API (WAAPI) provides a native browser API that Framer Motion partially builds upon. Libraries like Lottie render After Effects animations as JSON, ideal for complex illustrative animations. And newer entrypoints like CSS scroll-driven animations (the animation-timeline property) may eventually reduce the need for JavaScript-based scroll animations entirely.

The trend is clear: browsers are absorbing capabilities that previously required libraries. But GSAP and Framer Motion will remain relevant because they solve orchestration and developer experience problems that raw browser APIs do not address. A CSS transition can fade an element in. GSAP can orchestrate fifty elements fading, scaling, and rotating in a precisely choreographed sequence triggered by scroll position, with physics-based easing, responsive breakpoint adjustments, and accessible fallbacks — in a few dozen lines of readable code.

When planning and executing complex web projects that combine animation with broader development workflows, having a structured approach to web development project management ensures that animation work integrates smoothly with the rest of your frontend pipeline.

Conclusion

Web animation is no longer a decorative afterthought — it is a fundamental layer of user experience that communicates hierarchy, provides feedback, and creates emotional resonance. GSAP and Framer Motion represent two mature, production-proven approaches to this challenge. GSAP offers unmatched power, precision, and framework flexibility. Framer Motion offers elegant React integration, magical layout animations, and a declarative model that makes simple animations trivially easy.

The best choice depends on your project’s specific needs: framework, complexity, team skills, and the types of animation your design requires. In many cases, using both strategically — Framer Motion for component-level interactions and GSAP for scroll-driven orchestration — provides the most comprehensive animation toolkit. Whichever you choose, prioritize compositor-friendly properties, respect reduced-motion preferences, and profile on real devices. The goal is not just motion, but meaningful motion that enhances the experience for every user.

Frequently Asked Questions

Can I use GSAP and Framer Motion together in the same React project?

Yes, using both libraries in the same React project is a viable and sometimes optimal strategy. Framer Motion handles component-level animations like mount/unmount transitions, hover states, and layout shifts, while GSAP handles scroll-driven sequences, complex timelines, and SVG morphing through its useGSAP hook. The critical rule is to never animate the same CSS property on the same DOM element with both libraries simultaneously, as this creates conflicts. Assign clear responsibilities: Framer Motion for declarative, state-driven animations and GSAP for imperative, timeline-based orchestration.

Which animation library has better performance — GSAP or Framer Motion?

Both libraries deliver excellent performance for typical use cases. GSAP has a slight edge in raw animation throughput because it manipulates the DOM directly, bypassing React’s reconciliation cycle. Framer Motion’s layout animations use the FLIP technique, which can cause brief measurement overhead when animating many elements simultaneously. However, the performance difference is negligible for most projects. The bigger performance factor is which properties you animate — both libraries perform best when animating transforms and opacity, which are handled by the GPU compositor without triggering layout or paint operations.

How do I make web animations accessible for users with motion sensitivities?

Both GSAP and Framer Motion support the prefers-reduced-motion media query. In GSAP, check window.matchMedia("(prefers-reduced-motion: reduce)").matches and conditionally disable or simplify animations. Framer Motion provides a built-in useReducedMotion() hook that returns a boolean. When reduced motion is preferred, replace sliding and scaling animations with simple opacity fades or remove animation entirely. Beyond technical implementation, apply motion thoughtfully — animate only when it communicates meaningful information such as spatial relationships, action confirmation, or attention guidance.

What is the best approach for scroll-triggered animations on the web?

GSAP’s ScrollTrigger plugin is the current gold standard for scroll-triggered animations. It maps animation progress directly to scroll position, enabling scrubbing effects where elements animate in sync with scrolling. It handles edge cases like reverse scrolling, mobile overscroll bounce, and dynamic content height changes. For simpler visibility-based triggers, the native Intersection Observer API or Framer Motion’s whileInView prop are lighter alternatives. CSS scroll-driven animations (using animation-timeline) are an emerging native solution, but browser support is still maturing and the API covers fewer use cases than ScrollTrigger.

Is Framer Motion suitable for non-React projects like Vue or Angular?

Framer Motion was originally designed exclusively for React. However, the team has released a framework-agnostic version called Motion (previously “Motion One”) that works with vanilla JavaScript and can be integrated into Vue, Angular, or Svelte projects. That said, the vanilla version lacks some of React-specific features like AnimatePresence for exit animations and the layout prop for automatic FLIP animations. For non-React projects, GSAP remains the more mature and feature-complete option, offering identical functionality regardless of framework. If you are building with Vue or Angular, GSAP provides the most comprehensive animation capabilities.