12/6/2026 · 16 min read
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.
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.
transition: transform 200ms ease-out on a button is simpler, more maintainable, and performs identically to a JavaScript equivalent./* ✅ 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%;
}
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: 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 });
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.
layout prop + AnimatePresence handles animated list additions, removals, and reorders with almost zero code. Doing this in GSAP requires significant manual work.layoutId. It's genuinely hard with any other tool.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}
))}
);
}
@keyframes.layoutId.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 ... ;
}
All three tools can produce janky animations if used wrong. The rules are the same regardless of tool:
width, height, top, left or margin forces layout recalculation every frame and will drop to below 60fps on mid-range devices.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.content-visibility for off-screen elements.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