Resources
1Install
npx skillscat add qdhenry/claude-command-suite/claude-skills-gsap-animation Install via the SkillsCat registry.
Table of Contents
- Installation & TypeScript Setup
- Core Concepts
- Tweens
- Timelines
- Easing
- Staggers
- Control Methods
- Utility Methods
- Context & Cleanup
- Responsive Animations — matchMedia()
- Plugins Overview
- ScrollTrigger
- ScrollSmoother
- Flip Plugin
- SplitText Plugin
- React Integration — useGSAP()
- Performance Tips & Best Practices
- Helper Functions
Installation & TypeScript Setup
npm install gsapTypeScript definitions are bundled with the package. If you need to point your compiler to them explicitly:
// tsconfig.json
{
"compilerOptions": { ... },
"files": [
"node_modules/gsap/types/index.d.ts"
]
}Basic import:
import { gsap } from "gsap";Importing plugins:
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Flip } from "gsap/Flip";
import { SplitText } from "gsap/SplitText";
import { ScrollSmoother } from "gsap/ScrollSmoother";
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
// Register all plugins once, before use
gsap.registerPlugin(ScrollTrigger, Flip, SplitText, ScrollSmoother);Recommended: single gsap.ts barrel file to avoid duplicate registrations in large projects:
// gsap.ts
export * from "gsap";
export * from "gsap/ScrollTrigger";
export * from "gsap/Flip";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(ScrollTrigger, Flip);Then in other files:
import { gsap, ScrollTrigger } from "../gsap";UMD/dist format (for older build tools that don't support ES modules):
import { gsap } from "gsap/dist/gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";Tree shaking: Always call
gsap.registerPlugin(...)to prevent build tools from dropping plugins during tree shaking. It's safe to register the same plugin multiple times.
Core Concepts
GSAP has two primary animation primitives:
Tween — animates properties on target(s). Created with gsap.to(), gsap.from(), or gsap.fromTo().
Timeline — a container for sequencing multiple tweens and other timelines. Created with gsap.timeline().
Both extend an Animation base class and share the same control methods (play, pause, reverse, seek, timeScale, etc.).
GSAP can animate any numeric property of any JavaScript object — not just CSS. DOM elements, canvas contexts, WebGL uniforms, plain objects — anything works.
Transform shorthand — GSAP provides shorthand properties that map to CSS transforms:
| GSAP property | CSS equivalent |
|---|---|
x, y |
translateX, translateY |
xPercent, yPercent |
translateX(%), translateY(%) |
rotation |
rotate (degrees) |
rotationX, rotationY |
rotateX, rotateY |
scale, scaleX, scaleY |
scale |
skewX, skewY |
skew |
Tweens
// Animate TO values
gsap.to(".selector", {
x: 100,
y: 50,
rotation: 360,
backgroundColor: "red", // camelCase CSS
duration: 1, // seconds (default: 0.5)
delay: 0.5,
ease: "power2.inOut",
stagger: 0.1, // offset start per target
paused: false,
overwrite: "auto", // "auto" | true | false
repeat: 2, // -1 = infinite
repeatDelay: 1,
repeatRefresh: true, // re-evaluate dynamic values each repeat
yoyo: true, // A→B→A ping-pong
yoyoEase: "power1.in", // separate ease for reverse
immediateRender: false,
onStart: () => {},
onUpdate: () => {},
onComplete: () => {},
onRepeat: () => {},
onReverseComplete: () => {},
});
// Animate FROM values (immediateRender: true by default)
gsap.from(".selector", { x: -200, opacity: 0, duration: 1 });
// Animate from → to (define both explicitly)
gsap.fromTo(
".selector",
{ x: -200, opacity: 0 },
{ x: 0, opacity: 1, duration: 1 },
);
// Set immediately (no animation)
gsap.set(".selector", { x: 100, opacity: 0 });Function-based values — called once per target, returning the value to use:
gsap.to(".box", {
x: (index, target, targets) => index * 100,
duration: 1,
});Random values:
gsap.to(".box", {
x: "random(-100, 100)", // random number in range
x: "random(-100, 100, 5)", // rounded to nearest 5
x: "random([0, 100, 200])", // random from array
});Relative values:
gsap.to(".box", { x: "+=50", rotation: "-=30" });Keyframes:
gsap.to(".box", {
keyframes: [
{ x: 100, duration: 1 },
{ y: 50, duration: 0.5 },
{ opacity: 0, duration: 0.5 },
],
});Special properties:
| Property | Description |
|---|---|
duration |
Duration in seconds (default 0.5) |
delay |
Delay before start (seconds) |
ease |
Easing function name or function |
stagger |
Start time offset per target |
repeat |
Number of repeats (-1 = infinite) |
repeatDelay |
Pause between repeats |
yoyo |
Ping-pong direction on repeat |
paused |
Start paused |
overwrite |
Kill conflicting tweens: "auto" or true |
immediateRender |
Render first frame immediately |
onComplete |
Callback on finish |
onStart |
Callback on start |
onUpdate |
Callback every tick |
onRepeat |
Callback on each repeat |
onReverseComplete |
Callback when reversed to start |
id |
Unique string ID for gsap.getById() |
data |
Arbitrary data attached to the tween |
callbackScope |
this scope for callbacks |
startAt |
Define starting values for any property |
keyframes |
Array of tween vars for sequential states |
Timelines
const tl = gsap.timeline({
delay: 0.5,
paused: false,
repeat: -1,
repeatDelay: 1,
yoyo: true,
defaults: {
// all child tweens inherit these
duration: 1,
ease: "power2.out",
},
onComplete: () => {},
});
// Chain tweens (sequential by default)
tl.to(".a", { x: 100 }).to(".b", { y: 200 }).to(".c", { rotation: 360 });Position parameter — controls where each tween is placed in the timeline:
tl.to(".a", { x: 100 }, 0.5); // absolute 0.5s from start
tl.to(".b", { x: 100 }, "+=0.3"); // 0.3s after previous ends
tl.to(".c", { x: 100 }, "-=0.2"); // overlap 0.2s with previous
tl.to(".d", { x: 100 }, "myLabel"); // at label position
tl.to(".e", { x: 100 }, "myLabel+=0.5"); // 0.5s after label
tl.to(".f", { x: 100 }, "<"); // same start as previous tween
tl.to(".g", { x: 100 }, "<0.2"); // 0.2s after start of previous
tl.to(".h", { x: 100 }, "-=50%"); // overlap half of new tween's durationLabels:
tl.addLabel("intro", 2); // add label at 2s
tl.seek("intro"); // jump playhead to labelNesting timelines:
function buildScene1() {
const tl = gsap.timeline();
tl.to(".a", { x: 100 }).to(".b", { y: 50 });
return tl;
}
const master = gsap.timeline();
master.add(buildScene1()).add(buildScene2(), "-=0.5"); // overlap slightlyEasing
ease: "none"; // linear
ease: "power1.out"; // default
ease: "power1.in";
ease: "power1.inOut";
// power1 through power4, circ, expo, sine — all have .in .out .inOut
ease: "elastic"; // springy
ease: "elastic.out(1, 0.3)"; // amplitude, period
ease: "back"; // overshoots slightly
ease: "back.out(1.7)"; // configurable overshoot
ease: "bounce"; // bouncy landing
ease: "steps(12)"; // stepped/frame-by-frame
// EasePack (import separately)
ease: "rough({ strength: 1, points: 20, randomize: true })";
ease: "slow(0.7, 0.7, false)";
ease: "expoScale(1, 2)";
// Custom (requires CustomEase plugin)
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(CustomEase);
CustomEase.create("myEase", "0.23, 1, 0.32, 1");
ease: "myEase";Set global defaults:
gsap.defaults({ ease: "power2.inOut", duration: 0.8 });Staggers
// Simple — seconds between each start
gsap.to(".box", { y: 100, stagger: 0.1 });
// Advanced stagger object
gsap.to(".box", {
y: 100,
stagger: {
each: 0.1, // delay between each (preferred over `amount`)
amount: 1, // total time split across all targets
from: "center", // "start" | "end" | "center" | "edges" | "random" | index
grid: "auto", // [rows, cols] or "auto"
axis: "x", // "x" | "y" — for grid-based staggers
ease: "power2.inOut",
repeat: -1, // repeat per sub-tween
yoyo: true,
},
});
// Function-based stagger (return total delay from start)
gsap.to(".box", {
y: 100,
stagger: (index, target, list) => index * 0.08,
});Control Methods
const anim = gsap.to(".box", { x: 100, duration: 2, paused: true });
// Playback
anim.play();
anim.pause();
anim.resume(); // respects current direction
anim.reverse();
anim.restart();
anim.timeScale(2); // 2 = double speed, 0.5 = half
anim.seek(1.5); // jump to time (seconds) or label
anim.progress(0.5); // jump to 50%
anim.totalProgress(0.75); // includes repeats
// State
anim.isActive(); // true if currently animating
anim.paused(); // getter
anim.paused(true); // setter
anim.reversed();
anim.duration();
anim.totalDuration(); // includes repeats
anim.time();
anim.totalTime();
// Destruction
anim.kill(); // stop & remove from parent
anim.revert(); // stop & restore to pre-animation state
anim.invalidate(); // flush recorded start/end values
// Promise
await anim.then(() => console.log("done"));
// Callbacks
anim.eventCallback("onComplete", () => {});
// Timeline-specific
tl.add(thing, position);
tl.call(fn, params, position);
tl.getChildren();
tl.clear();
tl.tweenTo("label", { duration: 0.5 });
tl.tweenFromTo("start", "end", { duration: 1 });Global kill helpers:
gsap.killTweensOf(".selector");
gsap.killTweensOf(myElement);
gsap.killTweensOf(myFunction); // kills delayedCallsquickSetter & quickTo:
// quickSetter: fastest way to repeatedly SET a property (no animation)
const setX = gsap.quickSetter("#id", "x", "px");
document.addEventListener("mousemove", (e) => setX(e.clientX));
// quickTo: same but ANIMATES to the new value each call
const xTo = gsap.quickTo("#id", "x", { duration: 0.4, ease: "power3" });
document.addEventListener("mousemove", (e) => xTo(e.pageX));Global ticker:
gsap.ticker.add((time, deltaTime, frame) => {
// runs every GSAP tick
});
gsap.ticker.remove(myFunction);
gsap.ticker.fps(60); // cap frame rateUtility Methods
Accessible via gsap.utils.*. Many return reusable functions when called without a value argument.
gsap.utils.clamp(0, 100, 150); // → 100
gsap.utils.clamp(0, 100); // → (v) => clamp(v) — reusable fn
gsap.utils.mapRange(-10, 10, 0, 100, 5); // → 75
gsap.utils.normalize(100, 200, 150); // → 0.5 (maps to 0–1)
gsap.utils.interpolate("red", "blue", 0.5); // → "rgba(128,0,128,1)"
gsap.utils.interpolate([0, 100], 0.25); // → 25
gsap.utils.snap(5, 13); // → 15 (nearest increment of 5)
gsap.utils.snap([0, 50, 100], 73); // → 50 (nearest in array)
gsap.utils.random(0, 100); // → random number
gsap.utils.random(0, 100, 5); // → rounded to nearest 5
gsap.utils.random(["a", "b", "c"]); // → random element
gsap.utils.wrap(0, 10, 12); // → 2 (wraps around range)
gsap.utils.wrapYoyo(0, 10, 12); // → 8 (ping-pong)
gsap.utils.pipe(gsap.utils.clamp(0, 100), gsap.utils.snap(5))(108); // → 100
gsap.utils.toArray(".boxes"); // → Array from selector/NodeList
gsap.utils.selector(myEl); // → scoped selector fn: sel(".box")
gsap.utils.shuffle([1, 2, 3, 4]); // in-place shuffle → [3,1,4,2]
gsap.utils.splitColor("red"); // → [255, 0, 0]
gsap.utils.getUnit("30px"); // → "px"
gsap.utils.unitize(gsap.utils.wrap(0, 100))("150px"); // → "50px"
gsap.utils.checkPrefix("transform"); // → vendor-prefixed string
gsap.utils.distribute({ amount: 1, from: "center", ease: "power2" });Context & Cleanup
gsap.context() collects all animations/ScrollTriggers created within it so they can all be reverted at once. Essential for component-based frameworks.
const ctx = gsap.context(() => {
gsap.to(".box", { x: 100 });
gsap.timeline().to(".item", { y: 50 });
ScrollTrigger.create({ ... });
// all of the above are tracked
}, myContainerElement); // optional scope for selector text
// Later (e.g. on component unmount):
ctx.revert(); // all animations reverted, elements restored
// Add context-safe functions for event handlers
const ctx = gsap.context((self) => {
self.add("onClick", () => {
gsap.to(".box", { rotation: 360 }); // tracked!
});
}, containerRef);
button.addEventListener("click", ctx.onClick);Responsive Animations — matchMedia()
const mm = gsap.matchMedia();
// Simple breakpoints
mm.add("(min-width: 800px)", () => {
gsap.to(".box", { x: 500 });
ScrollTrigger.create({ ... });
return () => {
// optional custom cleanup when query stops matching
};
});
mm.add("(max-width: 799px)", () => {
gsap.to(".box", { x: 100 });
});
// Conditions syntax (shared setup code)
mm.add(
{
isDesktop: "(min-width: 800px)",
isMobile: "(max-width: 799px)",
reduceMotion: "(prefers-reduced-motion: reduce)",
},
(context) => {
const { isDesktop, reduceMotion } = context.conditions;
gsap.to(".box", {
rotation: isDesktop ? 360 : 180,
duration: reduceMotion ? 0 : 1.5,
});
}
);
// Revert all
mm.revert();Plugins Overview
Register plugins before use: gsap.registerPlugin(PluginA, PluginB).
| Plugin | Purpose |
|---|---|
| ScrollTrigger | Scroll-based animation triggers, scrubbing, pinning |
| ScrollSmoother | Native-scroll-based smooth scrolling (requires ScrollTrigger) |
| SplitText | Split text into chars/words/lines for animation |
| Flip | Seamless layout/DOM-change transitions (FLIP technique) |
| Draggable | Drag & drop with physics/bounds |
| Inertia | Momentum/glide after release (requires Draggable) |
| Observer | Unified pointer/touch/scroll event observer |
| MotionPath | Animate along SVG or custom paths |
| MorphSVG | Morph between SVG shapes |
| DrawSVG | Animate SVG stroke drawing |
| CustomEase | Create arbitrary cubic-bezier eases |
| CustomBounce | Create custom bounce eases |
| CustomWiggle | Create custom wiggle eases |
| EasePack | Extra eases: rough, slow, expoScale |
| ScrollTo | Animate the scroll position of any element |
| TextPlugin | Animate text content character by character |
| ScrambleText | Scramble/randomize text during animation |
| Physics2D | Physics-based 2D motion |
| PhysicsProps | Physics-based property animation |
| GSDevTools | Visual timeline debugger (dev only) |
| PixiPlugin | Animate PixiJS display objects |
| EaselPlugin | Animate EaselJS display objects |
ScrollTrigger
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);Simple trigger:
gsap.to(".box", {
scrollTrigger: ".box", // shorthand: trigger selector
x: 500,
});Full config on a timeline:
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
start: "top top", // [trigger edge] [scroller edge]
end: "+=500", // relative: 500px beyond start
scrub: 1, // seconds to "catch up" (true = instant)
pin: true, // pin the trigger element
markers: true, // dev-only visual markers
anticipatePin: 1, // counteract fast-scroll flash
snap: {
snapTo: "labels", // or number/array/fn
duration: { min: 0.2, max: 3 },
ease: "power1.inOut",
},
toggleActions: "play pause resume reset",
// actions: play | pause | resume | reset | restart | complete | reverse | none
// order: onEnter onLeave onEnterBack onLeaveBack
toggleClass: "active",
once: true, // kill after first activation
horizontal: false,
invalidateOnRefresh: true,
fastScrollEnd: true,
preventOverlaps: true,
onEnter: (self) => {},
onLeave: (self) => {},
onEnterBack: (self) => {},
onLeaveBack: (self) => {},
onUpdate: (self) => {},
onToggle: (self) => {},
onRefresh: (self) => {},
onScrubComplete: (self) => {},
},
});Standalone ScrollTrigger:
ScrollTrigger.create({
trigger: "#section",
start: "top center",
end: "bottom center",
onToggle: (self) => console.log("active:", self.isActive),
onUpdate: (self) => console.log("progress:", self.progress),
});Key static methods:
ScrollTrigger.refresh(); // recalculate all positions
ScrollTrigger.getAll(); // array of all instances
ScrollTrigger.getById("id");
ScrollTrigger.killAll();
ScrollTrigger.defaults({ markers: true }); // set defaults
// Batch — coordinate multiple triggers that fire around the same time
ScrollTrigger.batch(".card", {
onEnter: (elements) => gsap.from(elements, { y: 50, opacity: 0, stagger: 0.1 }),
start: "top 85%",
});
// Responsive — use matchMedia instead of ScrollTrigger.matchMedia (deprecated)
const mm = gsap.matchMedia();
mm.add("(min-width: 800px)", () => {
ScrollTrigger.create({ ... });
});start/end position syntax:
"top top" → top of trigger meets top of viewport
"center center" → midpoints meet
"bottom 80%" → bottom of trigger hits 80% down viewport
"+=300" → 300px beyond start
"top bottom-=100px" → top of trigger hits 100px above bottom of viewportScrollSmoother
Requires ScrollTrigger. Adds native-scroll smooth scrolling via CSS transforms.
Required HTML structure:
<body>
<div id="smooth-wrapper">
<div id="smooth-content">
<!-- ALL content here -->
</div>
</div>
<!-- position:fixed elements outside the wrapper -->
</body>import { ScrollSmoother } from "gsap/ScrollSmoother";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
// Create BEFORE any ScrollTriggers
const smoother = ScrollSmoother.create({
smooth: 1, // seconds to catch up to native scroll
effects: true, // enable data-speed / data-lag attributes
smoothTouch: 0.1, // smoothing on touch devices (default: none)
normalizeScroll: true, // prevent mobile address bar jump
ignoreMobileResize: true,
});Parallax & lag via data attributes:
<div data-speed="0.5">half scroll speed</div>
<div data-speed="2">double scroll speed</div>
<div data-speed="auto">auto-parallax (fill parent)</div>
<div data-lag="0.5">lags 0.5s behind scroll</div>
<div data-speed="clamp(0.5)">clamped — starts at native position</div>JavaScript effects:
smoother.effects(".box", { speed: 0.5, lag: 0.2 });Useful methods:
ScrollSmoother.get(); // get the singleton instance
smoother.scrollTo(".section", true, "top top");
smoother.scrollTop(500);
smoother.paused(true); // halt all scrolling
smoother.smooth(2); // change smooth time on the fly
smoother.getVelocity();
smoother.kill();Flip Plugin
FLIP = First, Last, Invert, Play. Animate seamlessly between two DOM states, even across layout or DOM changes.
import { Flip } from "gsap/Flip";
gsap.registerPlugin(Flip);Three-step pattern:
// 1. Capture current state
const state = Flip.getState(".targets");
// Optionally capture extra CSS props
const state = Flip.getState(".targets", { props: "backgroundColor,color" });
// 2. Make any DOM/CSS changes
element.classList.toggle("expanded");
container.appendChild(element);
// 3. Animate from old state to new
Flip.from(state, {
duration: 0.6,
ease: "power1.inOut",
absolute: true, // use position:absolute during flip
nested: true, // handle parent+child being flipped together
scale: false, // use width/height (default) vs scaleX/scaleY
fade: true, // crossfade when swapping elements
toggleClass: "flipping",
zIndex: 100,
onEnter: (elements) => gsap.fromTo(elements, { opacity: 0 }, { opacity: 1 }),
onLeave: (elements) => gsap.to(elements, { opacity: 0 }),
onComplete: () => {},
});Swapping two different elements — give them matching data-flip-id attributes:
<div class="card" data-flip-id="card-1"></div>
<!-- somewhere else in DOM -->
<div class="card-expanded" data-flip-id="card-1"></div>Other Flip methods:
Flip.to(state, vars); // animate TO a saved state (reverse)
Flip.fit(target, destination); // resize/reposition target to match destination
Flip.isFlipping(element); // check if actively flipping
Flip.killFlipsOf(targets);
Flip.makeAbsolute(targets); // position:absolute while keeping visual positionSplitText Plugin
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);Basic usage:
const split = SplitText.create(".headline", {
type: "lines, words, chars", // what to split into
});
// Returns arrays of newly created elements
split.chars; // Array of character <div>s
split.words; // Array of word <div>s
split.lines; // Array of line <div>s
// Animate them
gsap.from(split.chars, {
y: 60,
opacity: 0,
stagger: 0.03,
duration: 0.8,
ease: "back.out",
});
// Revert to original HTML when done
split.revert();Recommended v3.13+ pattern with autoSplit:
SplitText.create(".headline", {
type: "lines, words",
mask: "lines", // wraps lines with overflow:clip for reveal effects
autoSplit: true, // re-splits on font load or resize
onSplit(self) {
return gsap.from(self.lines, {
yPercent: 100,
opacity: 0,
stagger: 0.1,
duration: 0.8,
}); // returned animation is synced on re-split
},
});Key config options:
| Option | Description |
|---|---|
type |
"chars", "words", "lines" (comma-separated) |
mask |
"chars" | "words" | "lines" — adds overflow clip wrapper |
autoSplit |
Re-splits on font load or container resize |
onSplit |
Callback on each split; return animation for auto-sync |
linesClass |
CSS class for line elements ("line++" = auto-increment) |
wordsClass |
CSS class for word elements |
charsClass |
CSS class for char elements |
aria |
"auto" (default) | "hidden" | "none" |
deepSlice |
Handle nested elements (<strong>, <a>) spanning lines |
tag |
Wrapper tag, default "div" (use "span" for inline) |
reduceWhiteSpace |
Collapse whitespace (default true) |
ignore |
Selector for elements to skip splitting |
Accessibility: By default, SplitText adds
aria-labelto the parent andaria-hiddento split children, so screen readers read the whole text correctly.
Performance tip: Only split what you animate. Splitting thousands of nodes is expensive.
Custom fonts: Use
autoSplit: trueor wrap indocument.fonts.ready.then(() => {...})to avoid layout shift.
React Integration — useGSAP()
npm install @gsap/reactimport { useRef } from "react";
import { gsap } from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP); // prevents React version discrepancies
function MyComponent() {
const container = useRef<HTMLDivElement>(null);
useGSAP(
() => {
// All animations here are automatically reverted on unmount
gsap.to(".box", { x: 360, duration: 1 });
},
{ scope: container }, // scopes selector text to container
);
return (
<div ref={container}>
<div className="box" />
</div>
);
}Config options:
useGSAP(() => { ... }, {
dependencies: [value], // re-run when these change (like useEffect deps)
scope: containerRef, // scope selector text
revertOnUpdate: true, // revert & re-run on every dependency change
});Event handlers (context-safe):
Animations created inside event handlers run AFTER useGSAP() executes and won't be auto-cleaned up — wrap them in contextSafe:
// Option 1: destructure contextSafe from return value (for handlers outside the hook)
const { contextSafe } = useGSAP({ scope: container });
const onClick = contextSafe(() => {
gsap.to(".box", { rotation: 360 }); // safely tracked
});
return <button onClick={onClick} />;// Option 2: use 2nd argument inside the hook (for manual event listeners)
useGSAP(
(context, contextSafe) => {
gsap.to(".box", { x: 100 }); // safe
const onHover = contextSafe(() => {
gsap.to(".box", { scale: 1.2 }); // safe
});
boxRef.current.addEventListener("mouseenter", onHover);
return () => {
boxRef.current.removeEventListener("mouseenter", onHover);
};
},
{ scope: container },
);React 18 Strict Mode: GSAP effects run twice in dev.
useGSAP()handles the cleanup correctly so animations don't double-fire. Never use plainuseEffectfor GSAP without manual cleanup.
SSR / Next.js:
useGSAP()is SSR-safe. In App Router, add"use client"at the top of the file.
Performance Tips & Best Practices
Prefer transform properties — x, y, rotation, scale are GPU-composited and far cheaper than animating top, left, width, etc.
Use will-change sparingly — only on elements actively animating; too many defeats the purpose.
overwrite: "auto" — prevents conflicting tweens fighting each other without killing unrelated animations.
Register plugins once at app root — not inside components.
Kill or revert animations on cleanup — use gsap.context() or useGSAP() to avoid memory leaks.
Use gsap.quickSetter for high-frequency updates (mousemove, scroll) instead of repeated gsap.set() calls.
Use gsap.quickTo for high-frequency animated updates (pointer following, real-time sliders).
Avoid animating display or visibility directly — use autoAlpha (animates both opacity and visibility together).
autoAlpha — GSAP shorthand: opacity: 0 + visibility: hidden when 0, visibility: visible when > 0.
clearProps — remove inline styles after animation:
gsap.set(".box", { clearProps: "transform" }); // remove transforms
gsap.set(".box", { clearProps: "all" }); // remove all inline stylesSet defaults globally or per-timeline:
gsap.defaults({ ease: "power2.out", duration: 0.6 });
const tl = gsap.timeline({ defaults: { ease: "back.out", duration: 0.4 } });Register effects for reuse:
gsap.registerEffect({
name: "fadeIn",
effect: (targets, config) =>
gsap.from(targets, { opacity: 0, y: 30, duration: config.duration }),
defaults: { duration: 0.8 },
extendTimeline: true, // tl.fadeIn(".box") works!
});
gsap.effects.fadeIn(".hero");
tl.fadeIn(".cards", { duration: 0.5 });Modifiers plugin — dynamically transform values each frame (useful for infinite loops, clamping, etc.):
gsap.to(".box", {
x: 1000,
modifiers: {
x: gsap.utils.unitize((x) => parseFloat(x) % 500), // wrap at 500px
},
});Helper Functions
The GSAP team maintains an official collection of community helper functions at gsap.com/docs/v3/HelperFunctions. Notable ones:
| Helper | Purpose |
|---|---|
seamlessLoop() |
Infinite seamless carousel/ticker loop |
stopOverscroll() |
Prevent overscroll on iOS Safari |
lottieScrollTrigger() |
Tie a Lottie animation to scroll position |
blendEases() |
Blend two eases at start/end of animation |
distributeByPosition() |
Stagger irregular grid elements by position |
scrubCanvasFrames() |
Scrub through canvas image sequences on scroll |
anchorProgress() |
Progress values for SVG path anchor points |
weightedRandom() |
Biased random values using an ease curve |
callAfterResize() |
Debounced resize handler |
getScrollLookup() |
Get element scroll position (ScrollTrigger-aware) |
Quick Reference
// One-liner fade-in on scroll
gsap.from(".card", {
scrollTrigger: { trigger: ".card", start: "top 85%" },
opacity: 0,
y: 40,
duration: 0.8,
stagger: 0.1,
});
// Text reveal with SplitText
SplitText.create("h1", {
type: "lines",
mask: "lines",
autoSplit: true,
onSplit: (self) => gsap.from(self.lines, { yPercent: 100, stagger: 0.1 }),
});
// Pinned scroll section
gsap.timeline({
scrollTrigger: {
trigger: ".panel",
pin: true,
start: "top top",
end: "+=600",
scrub: 1,
},
}).to(".inner", { x: 400 }).to(".inner", { opacity: 0 });
// FLIP layout change
const state = Flip.getState(".item");
container.classList.toggle("grid-layout");
Flip.from(state, { duration: 0.6, ease: "power1.inOut", stagger:
Flip.from(state, { duration: 0.6, ease: "power1.inOut", stagger: 0.05 });
// Smooth scroll setup
ScrollSmoother.create({ smooth: 1, effects: true });
// Responsive animations
const mm = gsap.matchMedia();
mm.add("(min-width: 800px)", () => {
gsap.to(".box", { x: 500 });
});
mm.add("(prefers-reduced-motion: reduce)", () => {
gsap.globalTimeline.timeScale(0); // kill all motion
});