CSS animations have come a long way. With @keyframes, transitions, and now scroll-driven animations, you can build impressive effects without any JavaScript. So why would you reach for GSAP?

The answer isn’t “always use GSAP” or “CSS is enough.” It depends on what you’re building. This guide breaks down when each approach shines.

When CSS Animations Are the Right Choice

CSS handles many common animation needs elegantly:

Simple Hover States and Transitions

.button {
  transition: transform 0.2s ease, background 0.2s ease;
}

.button:hover {
  transform: translateY(-2px);
  background: #c8ff00;
}

For basic state changes—hovers, focus states, menu toggles—CSS transitions are simpler and perfectly performant. No library needed.

Loading Spinners and Loops

@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}

Infinite loops that don’t need JavaScript control are ideal CSS territory.

Entrance Animations on Scroll (Modern Browsers)

CSS scroll-driven animations (2024+) now handle basic scroll-triggered reveals:

@keyframes fade-in {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.card {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

For simple fade-ins on scroll, this works without JavaScript. Browser support is improving but not universal yet.

When GSAP Becomes Essential

GSAP isn’t about replacing CSS—it’s about doing what CSS can’t.

Complex Sequencing

Chaining animations in CSS requires calculating delays manually:

/* CSS approach - manual delay math */
.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
/* ... for every element */

GSAP handles this naturally:

gsap.from(".item", {
  y: 30,
  opacity: 0,
  stagger: 0.1,
  ease: "power2.out"
});

When you have 20 items, or the count is dynamic, GSAP’s approach scales effortlessly. The Stagger Grid Reveal effect demonstrates this pattern with a full grid of elements animating in sequence.

Timeline Control

Real applications need to pause, reverse, speed up, or seek animations:

const tl = gsap.timeline({ paused: true });

tl.to(".modal", { opacity: 1, duration: 0.3 })
  .from(".modal-content", { y: 20, duration: 0.3 }, "-=0.1")
  .from(".modal-buttons", { opacity: 0, stagger: 0.1 });

// Later: control it
openButton.addEventListener("click", () => tl.play());
closeButton.addEventListener("click", () => tl.reverse());

CSS has no equivalent. You’d need to toggle classes and hope timing aligns.

Scroll-Linked Animations (Scrubbing)

ScrollTrigger connects animation progress directly to scroll position:

gsap.to(".progress-bar", {
  width: "100%",
  ease: "none",
  scrollTrigger: {
    trigger: ".content",
    start: "top top",
    end: "bottom bottom",
    scrub: true  // Animation progress = scroll progress
  }
});

The animation doesn’t just trigger on scroll—it follows the scroll. CSS scroll-driven animations can do basic versions of this, but GSAP offers pinning, callbacks, snapping, and far more control. For a deep dive into all three approaches to scroll animation, see Animate on Scroll: 3 Approaches Compared.

For an example of scroll-linked animation at its most visual, the Scroll Image Sequence effect renders frame-by-frame sequences tied to scroll position.

Animating Non-CSS Properties

GSAP animates anything numeric:

// Animate a counter
gsap.to(counterObj, {
  value: 1000,
  duration: 2,
  onUpdate: () => {
    element.textContent = Math.round(counterObj.value);
  }
});

// Animate SVG path morphing (see the liquid morph effect)
gsap.to("#path", { morphSVG: "#targetPath" });

// Animate Three.js objects
gsap.to(mesh.position, { x: 5, y: 2, duration: 1 });

CSS can only animate CSS properties. GSAP animates any JavaScript value—SVG morphing, canvas objects, even text scramble effects that cycle through random characters.

Responsive Animation Differences

Different animations for different screen sizes:

let mm = gsap.matchMedia();

mm.add("(min-width: 768px)", () => {
  // Desktop: horizontal scroll gallery
  gsap.to(".gallery", {
    x: () => -galleryWidth,
    scrollTrigger: { scrub: true }
  });
});

mm.add("(max-width: 767px)", () => {
  // Mobile: simple fade-in cards
  gsap.from(".card", {
    opacity: 0,
    y: 30,
    stagger: 0.1,
    scrollTrigger: { trigger: ".gallery" }
  });
});

GSAP’s matchMedia properly creates and destroys animations at breakpoints. CSS media queries can change values, but managing complex scroll animations responsively is painful without JavaScript.

Performance: The Real Story

A common misconception: “CSS animations are always faster than JavaScript.”

The truth is more nuanced:

What actually matters for performance:

  • Animating transform and opacity (both CSS and GSAP do this)
  • Avoiding layout thrashing (reflows)
  • Minimizing paint areas
  • Running on the compositor thread when possible

GSAP animates the same optimized properties. Both can achieve 60fps. The difference isn’t usually measurable in practice.

Where CSS has an edge:

  • Very simple animations with no JS bundle cost
  • Animations that run when JavaScript is blocked

Where GSAP has an edge:

  • Complex sequences (less code, easier maintenance)
  • Coordinating many elements (better batching)
  • ScrollTrigger’s refresh/resize handling

For most real-world animations, the “performance” choice should be about maintainability, not milliseconds.

The Practical Decision Framework

Use CSS when:

  • Hover/focus state transitions
  • Simple infinite loops (spinners, pulses)
  • Basic entrance animations
  • You need zero JavaScript for that feature
  • Browser support for CSS scroll-driven animations meets your needs

Use GSAP when:

  • Sequencing multiple elements
  • You need play/pause/reverse control
  • Scroll-linked animations with scrubbing
  • Animating non-CSS values (counters, SVG morphs, canvas)
  • Complex responsive behavior
  • You need callbacks during animation
  • Cross-browser consistency matters

The hybrid approach: Many projects use both. CSS handles simple interactions; GSAP handles the showcase animations. There’s no rule that says you must pick one.

Getting Started with GSAP

If you’ve been CSS-only and want to try GSAP:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
gsap.registerPlugin(ScrollTrigger);

// Your first scroll animation
gsap.from(".hero-text", {
  y: 50,
  opacity: 0,
  duration: 1,
  scrollTrigger: {
    trigger: ".hero",
    start: "top 80%"
  }
});

That’s genuinely all it takes to get started. The GSAP documentation is excellent for going deeper.

Conclusion

CSS animations aren’t a “lesser” option, and GSAP isn’t overkill for everything. They solve different problems.

Use CSS for simple, stateless transitions. Use GSAP when you need sequencing, control, or scroll-linked behavior. Use both when it makes sense.

The best animation code is the code that’s maintainable, performs well, and achieves the effect you need. Sometimes that’s three lines of CSS. Sometimes that’s a GSAP timeline. Know when to reach for each.


Looking for production-ready GSAP effects? Browse the GSAP Vault effects library for scroll animations, text effects, and interactive components you can drop into any project.