Install any skill in seconds. Free to start, no credit card required.
Get Started Free →1. [Installation & TypeScript Setup](#installation--typescript-setup)
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 240% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 514% | 0% |
| case-22 | ✓→✓ | = Same ✓ | 813% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 429% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 507% | 0% |
bashnpm install gsap
TypeScript definitions are bundled with the package. If you need to point your compiler to them explicitly:
json// tsconfig.json { "compilerOptions": { ... }, "files": [ "node_modules/gsap/types/index.d.ts" ] }
Basic import:
tsimport { gsap } from "gsap";
Importing plugins:
tsimport { 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:
ts// 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:
tsimport { gsap, ScrollTrigger } from "../gsap";
UMD/dist format (for older build tools that don't support ES modules):
tsimport { 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.
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 |
ts// 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:
tsgsap.to(".box", { x: (index, target, targets) => index * 100, duration: 1, });
Random values:
tsgsap.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:
tsgsap.to(".box", { x: "+=50", rotation: "-=30" });
Keyframes:
tsgsap.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 |
tsconst 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:
tstl.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 duration
Labels:
tstl.addLabel("intro", 2); // add label at 2s tl.seek("intro"); // jump playhead to label
Nesting timelines:
tsfunction 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 slightly
tsease: "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:
tsgsap.defaults({ ease: "power2.inOut", duration: 0.8 });
ts// 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, });
tsconst 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:
tsgsap.killTweensOf(".selector"); gsap.killTweensOf(myElement); gsap.killTweensOf(myFunction); // kills delayedCalls
quickSetter & quickTo:
ts// 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:
tsgsap.ticker.add((time, deltaTime, frame) => { // runs every GSAP tick }); gsap.ticker.remove(myFunction); gsap.ticker.fps(60); // cap frame rate
Accessible via gsap.utils.*. Many return reusable functions when called without a value argument.
tsgsap.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" });
gsap.context() collects all animations/ScrollTriggers created within it so they can all be reverted at once. Essential for component-based frameworks.
tsconst 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);
tsconst 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();
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 |
tsimport { ScrollTrigger } from "gsap/ScrollTrigger"; gsap.registerPlugin(ScrollTrigger);
Simple trigger:
tsgsap.to(".box", { scrollTrigger: ".box", // shorthand: trigger selector x: 500, });
Full config on a timeline:
tsconst 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:
tsScrollTrigger.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:
tsScrollTrigger.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 viewportRequires ScrollTrigger. Adds native-scroll smooth scrolling via CSS transforms.
Required HTML structure:
html<body> <div id="smooth-wrapper"> <div id="smooth-content"> <!-- ALL content here --> </div> </div> <!-- position:fixed elements outside the wrapper --> </body>
tsimport { 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:
html<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:
tssmoother.effects(".box", { speed: 0.5, lag: 0.2 });
Useful methods:
tsScrollSmoother.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 = First, Last, Invert, Play. Animate seamlessly between two DOM states, even across layout or DOM changes.
tsimport { Flip } from "gsap/Flip"; gsap.registerPlugin(Flip);
Three-step pattern:
ts// 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:
html<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:
tsFlip.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 position
tsimport { SplitText } from "gsap/SplitText"; gsap.registerPlugin(SplitText);
Basic usage:
tsconst 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:
tsSplitText.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-label to the parent and aria-hidden to 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: true or wrap in document.fonts.ready.then(() => {...}) to avoid layout shift.
bashnpm install @gsap/react
tsximport { 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:
tsuseGSAP(() => { ... }, { 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:
tsx// 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} />;
tsx// 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 plain useEffect for GSAP without manual cleanup.
> SSR / Next.js: useGSAP() is SSR-safe. In App Router, add "use client" at the top of the file.
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:
tsgsap.set(".box", { clearProps: "transform" }); // remove transforms gsap.set(".box", { clearProps: "all" }); // remove all inline styles
Set defaults globally or per-timeline:
tsgsap.defaults({ ease: "power2.out", duration: 0.6 }); const tl = gsap.timeline({ defaults: { ease: "back.out", duration: 0.4 } });
Register effects for reuse:
tsgsap.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.):
tsgsap.to(".box", { x: 1000, modifiers: { x: gsap.utils.unitize((x) => parseFloat(x) % 500), // wrap at 500px }, });
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) |
ts// 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 });
Other measured skills in the registry, with their headline benchmark lift.