Full GSAP v3 mastery for interactive websites: core tweens/timelines, eases, staggers, keyframes, modifiers, utilities, plus complete plugin coverage (ScrollTrigger, ScrollTo, ScrollSmoother, Flip, Draggable, Inertia, Observer, MotionPath, DrawSVG, MorphSVG, SplitText, ScrambleText, TextPlugin, Physics2D/PhysicsProps, CustomEase/Wiggle/Bounce, GSDevTools). Includes Next.js/React patterns (useGSAP, gsap.context cleanup), responsive matchMedia, reduced-motion accessibility, performance best practices, and debugging playbooks.
You are a production-grade GSAP v3 engineer. You deliver interactive UI motion with:
GSAP Core handles Tween/Timeline + utilities/tools. Plugins add capabilities.
Licensing note: GSAP states the entire library is now free (historically some plugins were Club-only). Never recommend pirated/cracked plugins.
Auto-activate when the user mentions:
GSAP, Tween, Timeline, easing, stagger, keyframes, modifiers, ScrollTrigger, pin/scrub/snap, ScrollSmoother, ScrollTo, SplitText, ScrambleText, TextPlugin, Flip, Draggable, Inertia, Observer, MotionPath, MorphSVG, DrawSVG, physics, cursor follower, hover micro-interactions, Next.js/React cleanup, SSR, performance/jank, magnetic button, 3D tilt, card stack, swipe cards, add to cart, confetti, loading skeleton, tooltip, context menu, UI interactions, micro-interactions, gesture, pull to refresh, carousel snap, text animation, character animation, word animation, line reveal, typewriter, scramble text, typing effect, text split, staggered text, text mask reveal, gsap core, timeline control, utilities, quickTo, quickSetter, matchMedia, gsap.context, gsap.effects, ticker, GSDevTools, debugging, animation inspector, Physics2D, PhysicsProps, velocity, gravity, acceleration, friction, particles.
When asked to implement animations, output:
Prefer a "good MVP" first, then enhancements.
| Plugin | Description | Use Case |
|---|---|---|
| ScrollTrigger | Trigger/scrub/pin/snap animations on scroll | Most scroll animations |
| ScrollTo | Programmatic smooth scrolling | Navigation, CTA buttons |
| ScrollSmoother | Native-based smooth scrolling + parallax effects | Buttery smooth scroll feel |
| Observer | Unified wheel/touch/pointer gesture detection | Scroll-jacking, swipe gestures |
| Plugin | Description | Use Case |
|---|---|---|
| Flip | FLIP-based layout transitions | Grid reorder, modal expansion, shared-element |
| Draggable | Drag interactions | Carousels, sliders, cards |
| InertiaPlugin | Momentum/velocity glide | Throw physics after drag |
| Plugin | Description | Use Case |
|---|---|---|
| SplitText | Split chars/words/lines for animation | Staggered text reveals |
| ScrambleText | Randomized text decode effects | Techy headings |
| TextPlugin | Typing/replacing text content | Counters, dynamic labels |
| Plugin | Description | Use Case |
|---|---|---|
| DrawSVG | Animate stroke drawing | Line art, signatures |
| MorphSVG | Morph between SVG paths | Shape transitions |
| MotionPath | Animate along SVG paths | Following curves |
| MotionPathHelper | Visual path editing (dev tool) | Dev workflow |
| Plugin | Description | Use Case |
|---|---|---|
| Physics2D | 2D physics simulation | Ballistic motion, particles |
| PhysicsProps | Physics for any property | Natural property animation |
| CustomEase | Define custom easing curves | Signature motion language |
| CustomWiggle | Oscillating/wiggle eases | Shake, vibrate effects |
| CustomBounce | Custom bounce eases | Realistic bounces |
| EasePack | Extra built-in eases | Extended ease library |
| GSDevTools | Visual timeline debugging UI | Dev workflow |
| Plugin | Description | Use Case |
|---|---|---|
| PixiPlugin | Animate Pixi.js objects | Canvas/WebGL rendering |
| EaselPlugin | Animate EaselJS objects | CreateJS canvas |
| Helper | Description | Use Case |
|---|---|---|
| useGSAP() | Official React hook | React/Next.js apps |
gsap.to(target, { x: 100, duration: 1 }); // animate TO values
gsap.from(target, { opacity: 0 }); // animate FROM values
gsap.fromTo(target, { x: 0 }, { x: 100 }); // explicit from→to
gsap.set(target, { x: 0, opacity: 1 }); // instant set (no animation)
Prefer transform properties (x, y, scale, rotation) over layout props (top, left, width, height).
const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.6 } });
tl.from(".hero-title", { y: 30, autoAlpha: 0 })
.from(".hero-subtitle", { y: 20, autoAlpha: 0 }, "<0.1")
.from(".hero-cta", { scale: 0.9, autoAlpha: 0 }, "<0.15");
Rules:
defaults for consistency, override intentionallytl.to(a, {...}) // appends at end
tl.to(b, {...}, "<") // starts same time as previous
tl.to(c, {...}, ">") // starts after previous ends
tl.to(d, {...}, "+=0.3") // wait 0.3s after last end
tl.to(e, {...}, "label") // starts at label
snap utility for grid alignmentgsap.utils.mapRange(), clamp(), snap(), toArray()// ❌ BAD: Creates new tween every event
window.addEventListener("pointermove", (e) => {
gsap.to(".cursor", { x: e.clientX, y: e.clientY });
});
// ✅ GOOD: Reuses quickTo instances
const xTo = gsap.quickTo(".cursor", "x", { duration: 0.2, ease: "power3" });
const yTo = gsap.quickTo(".cursor", "y", { duration: 0.2, ease: "power3" });
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
// ✅ GOOD: Ultra-fast direct updates (no tween)
const setX = gsap.quickSetter(".cursor", "x", "px");
const setY = gsap.quickSetter(".cursor", "y", "px");
window.addEventListener("pointermove", (e) => { setX(e.clientX); setY(e.clientY); });
// ❌ BAD: Mixed reads and writes
elements.forEach(el => {
const rect = el.getBoundingClientRect(); // read
gsap.set(el, { x: rect.width }); // write
});
// ✅ GOOD: Batch reads, then writes
const rects = elements.map(el => el.getBoundingClientRect()); // all reads
elements.forEach((el, i) => gsap.set(el, { x: rects[i].width })); // all writes
invalidateOnRefresh when layout-driven values are usedScrollTrigger.batch() for lists of similar elementsScrollTrigger enables scroll-based triggering, scrubbing, pinning, snapping, etc.
// Pattern A: Inline scrollTrigger
gsap.to(".box", {
x: 500,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
end: "top 30%",
scrub: 1,
markers: true // dev only
}
});
// Pattern B: Timeline + ScrollTrigger.create
const tl = gsap.timeline();
tl.from(".item", { y: 50, autoAlpha: 0, stagger: 0.1 });
ScrollTrigger.create({
animation: tl,
trigger: ".section",
start: "top 70%",
toggleActions: "play none none reverse"
});
const tl = gsap.timeline();
tl.from(".panel1", { autoAlpha: 0, y: 20 })
.to(".panel1", { autoAlpha: 0 })
.from(".panel2", { autoAlpha: 0, y: 20 }, "<");
ScrollTrigger.create({
animation: tl,
trigger: ".story",
start: "top top",
end: "+=2000",
scrub: 1,
pin: true,
invalidateOnRefresh: true
});
ScrollTrigger.batch(".card", {
onEnter: (elements) => gsap.from(elements, { y: 30, autoAlpha: 0, stagger: 0.1 }),
start: "top 85%"
});
const sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".horizontal-container",
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector(".horizontal-container").offsetWidth
}
});
gsap.matchMedia().add({
"(prefers-reduced-motion: no-preference) and (min-width: 768px)": () => {
// Desktop animations
gsap.from(".hero", {
y: 50, autoAlpha: 0,
scrollTrigger: { trigger: ".hero", start: "top 80%" }
});
},
"(prefers-reduced-motion: reduce)": () => {
// Reduced motion: instant visibility, no animation
gsap.set(".hero", { autoAlpha: 1 });
}
});
Use ScrollTo for smooth navigation to anchors/sections.
gsap.registerPlugin(ScrollToPlugin);
// Scroll to element
gsap.to(window, { duration: 1, scrollTo: "#pricing", ease: "power2.out" });
// Scroll to position
gsap.to(window, { duration: 1, scrollTo: { y: 500, autoKill: true } });
// With offset
gsap.to(window, { scrollTo: { y: "#section", offsetY: 80 } });
ScrollSmoother creates native-scroll-based smoothing and parallax effects.
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
// Required HTML structure:
// <div id="smooth-wrapper">
// <div id="smooth-content">...content...</div>
// </div>
const smoother = ScrollSmoother.create({
wrapper: "#smooth-wrapper",
content: "#smooth-content",
smooth: 1.5, // seconds for smoothing
effects: true // enable data-speed/data-lag attributes
});
// HTML: <div data-speed="0.5">Slow parallax</div>
// <div data-speed="2">Fast parallax</div>
// <div data-lag="0.5">Trailing effect</div>
Gotchas:
refresh() after layout changesnormalizeScroll: true for mobile consistencyUse Observer to unify wheel/touch/pointer events into consistent "intent" signals.
gsap.registerPlugin(Observer);
Observer.create({
target: window,
type: "wheel,touch,pointer",
onUp: () => goToPrevSection(),
onDown: () => goToNextSection(),
tolerance: 50,
preventDefault: true
});
Use cases:
Use Flip when elements change layout (grid reorder, modal expansion, shared-element transitions).
gsap.registerPlugin(Flip);
// 1. Capture state
const state = Flip.getState(".cards");
// 2. Change layout (DOM/CSS)
container.classList.toggle("grid");
// 3. Animate from old state
Flip.from(state, {
duration: 0.6,
ease: "power2.inOut",
stagger: 0.05,
absolute: true,
onEnter: elements => gsap.fromTo(elements, { autoAlpha: 0 }, { autoAlpha: 1 }),
onLeave: elements => gsap.to(elements, { autoAlpha: 0 })
});
gsap.registerPlugin(Draggable, InertiaPlugin);
Draggable.create(".slider", {
type: "x",
bounds: ".container",
inertia: true, // requires InertiaPlugin
snap: value => Math.round(value / 200) * 200, // snap to 200px intervals
onDrag: function() { console.log(this.x); },
onThrowComplete: () => console.log("Throw complete")
});
DIY Momentum (if InertiaPlugin unavailable):
let velocity = 0;
let lastX = 0;
Draggable.create(".element", {
type: "x",
onDrag: function() {
velocity = this.x - lastX;
lastX = this.x;
},
onDragEnd: function() {
// DIY momentum with exponential decay
gsap.to(this.target, {
x: `+=${velocity * 10}`,
duration: Math.abs(velocity) * 0.1,
ease: "power2.out"
});
}
});
gsap.registerPlugin(SplitText);
const split = new SplitText(".headline", { type: "chars,words,lines" });
gsap.from(split.chars, {
y: 20,
autoAlpha: 0,
stagger: 0.02,
duration: 0.4,
ease: "power3.out"
});
// Cleanup (important for responsive)
// split.revert();
DIY Alternative (if needed):
function splitIntoSpans(el) {
const text = el.textContent;
el.innerHTML = text.split('').map(char =>
char === ' ' ? ' ' : `<span class="char">${char}</span>`
).join('');
return el.querySelectorAll('.char');
}
gsap.registerPlugin(ScrambleTextPlugin);
gsap.to(".title", {
duration: 1.5,
scrambleText: {
text: "DECODED MESSAGE",
chars: "XO",
revealDelay: 0.5
}
});
gsap.registerPlugin(TextPlugin);
gsap.to(".counter", {
duration: 2,
text: { value: "100%", delimiter: "" },
ease: "none"
});
gsap.registerPlugin(DrawSVGPlugin);
gsap.from(".line-path", {
drawSVG: 0, // or "0%" / "50% 50%"
duration: 2,
ease: "power2.inOut"
});
gsap.registerPlugin(MorphSVGPlugin);
gsap.to("#shape1", {
morphSVG: "#shape2",
duration: 1,
ease: "power2.inOut"
});
gsap.registerPlugin(MotionPathPlugin);
gsap.to(".element", {
duration: 5,
motionPath: {
path: "#curve-path",
align: "#curve-path",
alignOrigin: [0.5, 0.5],
autoRotate: true
},
ease: "none"
});
gsap.registerPlugin(Physics2DPlugin, PhysicsPropsPlugin);
// 2D physics (velocity, angle, acceleration, gravity)
gsap.to(".ball", {
duration: 3,
physics2D: {
velocity: 500,
angle: -60,
gravity: 500
}
});
// Physics for any property
gsap.to(".element", {
duration: 2,
physicsProps: {
x: { velocity: 100, acceleration: 50 },
rotation: { velocity: 360 }
}
});
gsap.registerPlugin(CustomEase, CustomWiggle, CustomBounce);
// Custom SVG path ease
CustomEase.create("myEase", "M0,0 C0.25,0.1 0.25,1 1,1");
gsap.to(".el", { x: 100, ease: "myEase" });
// Wiggle ease
CustomWiggle.create("myWiggle", { wiggles: 6, type: "easeOut" });
gsap.to(".el", { rotation: 20, ease: "myWiggle" });
// Custom bounce
CustomBounce.create("myBounce", { strength: 0.7, squash: 2 });
gsap.to(".el", { y: 300, ease: "myBounce" });
gsap.registerPlugin(GSDevTools);
// Attach to a specific timeline
const tl = gsap.timeline();
tl.to(".box", { x: 500 })
.to(".box", { rotation: 360 });
GSDevTools.create({ animation: tl });
// Or attach to global timeline
GSDevTools.create();
Use gsap.context() to scope selectors and revert everything.
"use client";
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export function SectionAnim() {
const root = useRef(null);
useLayoutEffect(() => {
if (!root.current) return;
const ctx = gsap.context(() => {
const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.8 } });
tl.from(".title", { y: 18, autoAlpha: 0 })
.from(".item", { y: 14, autoAlpha: 0, stagger: 0.06 }, "<0.1");
gsap.from(".reveal", {
y: 24, autoAlpha: 0,
scrollTrigger: { trigger: ".reveal", start: "top 85%" }
});
}, root);
return () => ctx.revert();
}, []);
return (
<section ref={root}>
<h2 className="title">Title</h2>
<div className="item">A</div>
<div className="item">B</div>
<div className="reveal">Reveal</div>
</section>
);
}
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import ScrollTrigger from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export function AnimatedSection() {
const container = useRef(null);
useGSAP(() => {
gsap.from(".box", {
y: 50,
autoAlpha: 0,
scrollTrigger: { trigger: ".box", start: "top 80%" }
});
}, { scope: container });
return (
<div ref={container}>
<div className="box">Content</div>
</div>
);
}
window/document outside effectstypeof window !== 'undefined' guards when neededuseLayoutEffect(() => {
const mm = gsap.matchMedia();
mm.add("(min-width: 768px)", () => {
// Desktop animations
gsap.from(".hero", { x: -100 });
return () => { /* cleanup */ };
});
mm.add("(max-width: 767px)", () => {
// Mobile animations
gsap.from(".hero", { y: 50 });
return () => { /* cleanup */ };
});
return () => mm.revert();
}, []);
transform/opacity/visibility/display/transition rules.gsap.context() and ctx.revert() to avoid stacking tweens/ScrollTriggers.<, >, +=0.2) instead of manual delays.gsap.globalTimeline.timeScale(0.5).markers: true (dev only).ScrollTrigger.refresh() after images/fonts/dynamic content settle.scroller consistently when not using window.anticipatePin, check layout shift, consider invalidateOnRefresh.quickTo/quickSetter.top/left/width/height where possible.If user cannot use a plugin (policy/offline constraints):
Never suggest cracked/pirated plugins.
const el = document.querySelector(".btn");
el.addEventListener("mouseenter", () =>
gsap.to(el, { scale: 1.03, duration: 0.2, overwrite: "auto" })
);
el.addEventListener("mouseleave", () =>
gsap.to(el, { scale: 1.00, duration: 0.25, overwrite: "auto" })
);
const xTo = gsap.quickTo(".cursor", "x", { duration: 0.2, ease: "power3" });
const yTo = gsap.quickTo(".cursor", "y", { duration: 0.2, ease: "power3" });
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
const btn = document.querySelector(".magnetic");
const strength = 0.3;
btn.addEventListener("mousemove", (e) => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
gsap.to(btn, { x: x * strength, y: y * strength, duration: 0.3 });
});
btn.addEventListener("mouseleave", () => {
gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: "elastic.out(1, 0.3)" });
});
gsap.from(".reveal", {
y: 24, autoAlpha: 0, duration: 0.8, ease: "power3.out",
scrollTrigger: {
trigger: ".reveal",
start: "top 85%",
toggleActions: "play none none reverse"
}
});
gsap.from(".list-item", {
y: 30,
autoAlpha: 0,
duration: 0.6,
stagger: 0.1,
ease: "power3.out",
scrollTrigger: { trigger: ".list", start: "top 80%" }
});
gsap.to(".hero-bg", {
yPercent: 30,
ease: "none",
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "bottom top",
scrub: true
}
});
See: