There are two animation religions in modern React. One says "animations are component state" — declare your motion inside JSX, let the framework handle the math. That's Framer Motion. The other says "animations are a scripting language" — imperatively tell the DOM what to do, frame by frame, tied to user input. That's GSAP. Both are right. Both are wrong. The answer, as always, is context.
If you're building a design system, a SaaS dashboard, or an interactive component library, Framer Motion is your tool. It's built for React. It's declarative. Gestures feel native. Page transitions animate without breaking Astro ViewTransitions. But if you're building a marketing site, a hero section that pins while content animates around it, or scroll-driven storytelling where motion is part of the pitch, GSAP is the standard that every premium site in the world uses. Stripe uses it. Apple uses it. Webflow uses it. And Aidxn uses both, context-dependent.
What's the Difference, Really?
Framer Motion is a React library. You import hooks, declare animation values in your JSX, and the component re-renders as the animation progresses. It's built on Popmotion (a lower-level animation engine) and was designed specifically for React developers who want motion without leaving component-land. It's 35kb minified + gzipped. It understands Tailwind classes, CSS variables, and transform properties. It has gesture detection built in. If you're animating a modal open, a button hover state, or a drag-to-reorder list, Framer feels native because it is native to React.
GSAP (GreenSock Animation Platform) is a framework-agnostic JavaScript library. You select DOM elements, tell GSAP what properties to animate, and it handles the math. You can use it in vanilla JavaScript, jQuery, React, Vue, Svelte, or a combination. GSAP is 35kb minified + gzipped (same size as Framer, actually). But it's optimized for different use cases: scroll-driven animations, timelines, morphing, pixel-perfect easing, and orchestration across dozens of elements. If you're pinning a section and scrubbing a timeline with scroll, GSAP is the only choice in production.
The Head-to-Head Comparison
Bundle Size: Framer Motion ~35kb. GSAP ~35kb. If you ship both (which Aidxn does), you're at ~70kb. Add in the GSAP ScrollTrigger plugin (+15kb) and you're at ~85kb. That's a single 4K image. Not a blocker. Modern sites ship 2–5MB. Animation libraries are noise in the budget.
Developer Experience: Framer Motion wins for React developers. It's JSX-first. You see the motion in your component. GSAP wins for orchestration. You write once, apply to multiple elements, and the timeline logic is explicit. Framer Motion feels faster to prototype (modal in 10 minutes). GSAP feels faster to iterate on complex sequences (change one easing curve, affects 5 animations).
Capabilities: Framer Motion covers 80% of React animation use cases — enter/exit animations, layout shifts, drag-drop, gesture detection, shared layout animations. GSAP covers the other 20% that Framer can't do: scroll-driven pinning, morphing, multi-element orchestration at the timeline level, and frame-by-frame control. The ceiling on GSAP is higher. You can do things with GSAP that Framer literally doesn't have primitives for.
Performance: Both are performant. Framer Motion uses the Web Animations API and CSS transforms when possible. GSAP uses requestAnimationFrame and is hyper-optimized for scroll events. On mobile, GSAP's `scrub: 1` (smooth) mode is actually more performant than Framer Motion's continuous re-renders because it throttles updates. Test in production. Measure. But in practice, neither is a bottleneck.
Three Patterns That Show the Difference
Pattern 1: Gesture-Driven Modal (Framer)
A modal slides in from the bottom on desktop. On mobile, it swipes in. Swipe down to close. Drag to reposition. This is pure React gesture interaction. Framer Motion is the right tool:
import { motion, AnimatePresence } from 'framer-motion';
export const ContactModal = ({ isOpen, onClose }) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
animate={{ opacity: 1, backdropFilter: 'blur(8px)' }}
exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
onClick={onClose}
className="fixed inset-0 bg-black/20"
>
<motion.div
initial={{ y: 600 }}
animate={{ y: 0 }}
exit={{ y: 600 }}
drag="y"
dragElastic={0.2}
onDragEnd={(event, info) => {
if (info.velocity.y > 500 || info.offset.y > 200) {
onClose();
}
}}
className="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-6"
>
<form className="space-y-4">
<input placeholder="Your name" />
<input placeholder="Your email" />
<button>Send</button>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
This modal slides in, has gesture detection (drag down to close), and handles enter/exit animations. It's declarative. It's React. Framer handles the gesture logic, momentum, and spring physics. You just declare intent in JSX.
Pattern 2: Scroll-Pinned Hero Timeline (GSAP)
A hero section pins to the viewport. As the user scrolls, a timeline of animations plays: headline fades in, background morphs, supporting copy cycles through 3 variants, CTA button animates to center stage. This is GSAP ScrollTrigger + Timeline. Framer Motion can't do this — it has no scroll-driven primitives:
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
import { useEffect, useRef } from 'react';
gsap.registerPlugin(ScrollTrigger);
export const HeroTimeline = () => {
const heroRef = useRef(null);
const headlineRef = useRef(null);
const bgRef = useRef(null);
const copyRef = useRef(null);
const ctaRef = useRef(null);
useEffect(() => {
const timeline = gsap.timeline();
timeline
.to(headlineRef.current, { opacity: 1, y: -30, duration: 1 }, 0)
.to(bgRef.current, { rotation: 15, scale: 1.1, duration: 1 }, 0)
.to(copyRef.current, { opacity: 1, duration: 0.8 }, 0.3)
.to(ctaRef.current, { scale: 1, duration: 0.5 }, 0.8);
ScrollTrigger.create({
trigger: heroRef.current,
animation: timeline,
start: 'top top',
end: 'bottom center',
scrub: 0.5,
pin: true,
markers: false
});
return () => {
ScrollTrigger.getAll().forEach(t => t.kill());
};
}, []);
return (
<section ref={heroRef} className="relative h-screen">
<div ref={bgRef} className="absolute inset-0 bg-gradient-to-r opacity-0" />
<div className="relative z-10 flex flex-col items-center justify-center h-full">
<h1 ref={headlineRef} className="text-6xl opacity-0">
Animation Mastery in 2026
</h1>
<p ref={copyRef} className="mt-8 text-xl opacity-0">
Ship motion that converts.
</p>
<button
ref={ctaRef}
className="mt-8 px-8 py-3 scale-0 bg-blue-600 text-white rounded"
>
Get Started
</button>
</div>
</section>
);
};
The timeline composes multiple animations. ScrollTrigger ties it to scroll position. Pinspacer holds the space automatically. As the user scrolls down, each animation plays in sequence, scrubbed by scroll velocity. Scroll back up and it animates in reverse. This is impossible with Framer Motion. Framer doesn't understand scroll-driven triggers or pinning. That's why GSAP owns the scroll-animation market.
Pattern 3: Staggered Enter Animation on Route Change (Framer)
User navigates from `/` to `/services`. The page loads. A grid of 12 cards stagger in (first card 0ms, second 50ms, third 100ms, etc). Each card slides up from below while opacity fades in. This is route-level animation, pure React state.
import { motion } from 'framer-motion';
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: { delay: i * 0.05, duration: 0.5 }
})
};
export const ServicesGrid = ({ services }) => {
return (
<motion.div
className="grid grid-cols-3 gap-6"
initial="hidden"
animate="visible"
variants={{ visible: { transition: { staggerChildren: 0 } } }}
>
{services.map((service, i) => (
<motion.div
key={service.id}
custom={i}
variants={cardVariants}
className="p-6 border rounded-lg"
>
<h3>{service.title}</h3>
<p>{service.description}</p>
</motion.div>
))}
</motion.div>
);
};
Each card gets a custom delay based on its index. Framer's `custom` prop lets you pass variables to variants. The motion is component-level. No DOM selection. No timeline management. Just React state flowing into motion. This is where Framer shines — it's natural to React developers.
Real Code: Aidxn Dual-Stack
Velocity X (Aidxn's process-driven template) uses Framer for modals and dashboard animations, GSAP for hero and section pinning. Here's the pattern:
// ContactModal.tsx (Framer)
export const ContactModal = ({ isOpen, onClose }) => {
return <AnimatePresence>{isOpen && <...motion.div.../>}</AnimatePresence>;
};
// HeroSticky.tsx (GSAP)
export const HeroSticky = () => {
useEffect(() => {
const timeline = gsap.timeline();
timeline.to(...);
ScrollTrigger.create({ animation: timeline, pin: true });
return () => ScrollTrigger.getAll().forEach(t => t.kill());
}, []);
return <section ref={...}>...</section>;
};
// Layout.astro (page orchestration)
<HeroSticky />
<FeaturesSection />
<ContactModal isOpen={contactOpen} onClose={() => setContactOpen(false)} />
GSAP for the hero. Framer for the modal. The page load animates with GSAP. The modal interaction animates with Framer. No fighting between libraries. Each does what it's designed to do. This is production.
Six FAQs
Isn't 70kb of animation libraries overkill?
No. A single 4K image is 2–8MB. A video is 50–200MB. 70kb is 0.5% of a typical page budget. The motion quality and time-to-ship savings are worth it. Ship both.
Can I use Framer Motion for scroll-driven animation?
Not really. Framer's scroll hooks are limited to triggering enter/exit states. If you need to scrub a timeline with scroll velocity or pin a section, use GSAP. Framer can't compete there.
Does GSAP work in Next.js / SvelteKit / Astro?
Yes. GSAP is framework-agnostic. Use it in a client component or script. Just initialize ScrollTrigger in a useEffect hook (or equivalent) and clean up on unmount. Works everywhere.
What if I only want to use one library?
Choose based on your primary use case. SaaS dashboard? Framer. Marketing site with scroll animations? GSAP. If you're 50/50, ship both. The size penalty is minimal.
How do I debug animation timing in Framer?
Use the React DevTools to inspect component state. Check the motion values (they're just numbers). Slow down the browser (DevTools → Performance). Framer's DevTools plugin is unofficial but helpful. For GSAP, use `markers: true` in ScrollTrigger and you'll see start/end positions on the page.
Which library is more maintainable long-term?
Both are battle-tested. Framer Motion is backed by Framer (private team, stable). GSAP is backed by GreenSock (been around for 20 years, not going anywhere). GSAP has a slight edge for longevity because it's not tied to a single framework. But both are safe bets.
The Bottom Line
Animation in 2026 isn't about choosing the "right" library. It's about choosing the right tool for the right problem. Framer Motion is excellent at what it does: React-native, gesture-friendly, component-level motion. GSAP is excellent at what it does: scroll orchestration, timeline control, premium storytelling.
The sites that feel expensive use both. Modals spring in (Framer). Scroll down and a hero section pins while a timeline animates (GSAP). The motion is intentional. The user experience is buttery smooth. And you're not wrestling with a library that wasn't designed for your use case.
Start with Framer if you're building an app. Start with GSAP if you're building a marketing site. If you're building both, reach out — that's when the real motion work begins. For deeper scroll-animation patterns, check out our post on GSAP ScrollTrigger pin and pinspacer, which covers the exact patterns that make premium sites convertible.