Skip to main content

GSAP (GreenSock Animation Platform)

GSAP (GreenSock Animation Platform) is a JavaScript animation library for tweening basically any property of any JS object — DOM styles, SVG attributes, canvas, Three.js objects, React state via setters, etc. It's framework-agnostic and works alongside React, Vue, Svelte, or vanilla JS.

Since 2024 GSAP is 100% free, including plugins that used to be "Club GreenSock only" (ScrollTrigger, SplitText, MorphSVG, DrawSVG, etc.) — Webflow acquired GreenSock and open-sourced everything.

Installation

npm install gsap
import { gsap } from "gsap";

For plugins, import and register them explicitly:

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(ScrollTrigger, useGSAP);

Forgetting to registerPlugin is the #1 cause of "my ScrollTrigger just doesn't do anything" bugs.

Core mental model

Everything in GSAP boils down to a tween: animate property(ies) of a target from one value to another over time. Timelines are just containers that sequence tweens.

MethodWhat it does
gsap.to(target, {})Animates from current value → given value
gsap.from(target, {})Animates from given value → current value (useful for "enter" animations)
gsap.fromTo(target, {}, {})Explicitly set both start and end state
gsap.set(target, {})Instantly sets properties, no animation (shorthand for a 0-duration tween)
gsap.to(".box", { x: 200, duration: 1, ease: "power2.out" });

gsap.from(".card", { opacity: 0, y: 40, duration: 0.6, stagger: 0.1 });

gsap.fromTo(
".logo",
{ scale: 0 },
{ scale: 1, duration: 0.5, ease: "back.out(1.7)" }
);

Common vars (properties) object

  • duration — seconds (default 0.5)
  • delay — seconds before start
  • ease — see Easing
  • repeat — number of repeats (-1 = infinite)
  • yoyo — reverses on alternate repeats (needs repeat)
  • stagger — delay between each element in a selection (number or object: { each, from, grid, amount })
  • onComplete, onStart, onUpdate, onRepeat — callbacks
  • x, y, xPercent, yPercent, rotation, scale, opacity — GSAP-optimized shorthand props that use transforms instead of triggering layout/reflow
tip

Prefer x/y (which map to translate) and xPercent/yPercent over animating left/top/margin. Transform-based animation is GPU accelerated and doesn't cause reflow — this is the single biggest perf win in GSAP.

Easing

Easing controls the rate of change over the tween's duration.

gsap.to(".box", { x: 300, ease: "power2.inOut" });

Common built-ins: "power1""power4" (subtle → strong), each with .in, .out, .inOut (e.g. "power2.out"), plus "back", "elastic", "bounce", "circ", "expo", "sine", and "none" (linear).

  • .out = fast start, slow finish — good for elements entering/settling
  • .in = slow start, fast finish — good for elements exiting
  • .inOut = slow-fast-slow — good for continuous/looping motion

Use the GSAP ease visualizer to preview these rather than guessing.

Timelines

A gsap.timeline() lets you sequence and overlap tweens with a shared playback controller (play/pause/reverse/seek the whole sequence at once).

const tl = gsap.timeline({ defaults: { duration: 0.5, ease: "power2.out" } });

tl.to(".title", { opacity: 1, y: 0 })
.to(".subtitle", { opacity: 1, y: 0 }, "-=0.3") // start 0.3s before previous ends
.to(".cta", { opacity: 1, y: 0 }, "<") // start at same time as previous
.to(".bg", { scale: 1 }, "+=0.2"); // start 0.2s after previous ends

Position parameter cheatsheet

The 3rd argument to .to()/.from()/.fromTo() in a timeline controls where the tween is inserted relative to the playhead:

ValueMeaning
(omitted)Right after the previous tween ends
"+=1"1 second after the previous tween ends
"-=0.5"0.5 seconds before the previous tween ends (overlap)
"<"At the start of the previous tween
">"At the end of the previous tween (same as default)
"1.5"At the absolute time 1.5s on the timeline
"myLabel"At a named label — set with tl.addLabel("myLabel")

Staggering multiple elements

gsap.to(".card", {
opacity: 1,
y: 0,
duration: 0.6,
stagger: {
each: 0.1,
from: "start", // "start" | "end" | "center" | "edges" | "random" | index
},
});

For grids: stagger: { each: 0.05, grid: "auto", from: "center" }.

ScrollTrigger

The most-used plugin: ties animation progress (or start/end) to scroll position.

import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);

gsap.to(".panel", {
x: -500,
scrollTrigger: {
trigger: ".panel",
start: "top center", // when top of trigger hits center of viewport
end: "bottom top",
scrub: true, // tie progress directly to scroll (or a number for smoothing, e.g. 1)
pin: true, // pin the trigger element while active
markers: true, // debug visual markers — REMOVE before shipping
},
});

Key scrollTrigger options:

  • trigger — element that starts/stops the animation (defaults to the tween's target)
  • start / end"<trigger-position> <viewport-position>", e.g. "top 80%"
  • scrubtrue (tightly linked) or a number (seconds of "catch-up" smoothing, e.g. 0.5)
  • pin — pins the element in place for the duration of the scroll trigger
  • toggleActions"play pause resume reset" style string for onEnter/onLeave/onEnterBack/onLeaveBack
  • markers — visual start/end markers, dev-only
caution

ScrollTrigger.refresh() is your friend when layout shifts (images loading, fonts swapping, content resizing) — otherwise trigger positions go stale. In React, this is usually handled automatically inside useGSAP's cleanup, but manual setups often need it wired to window.addEventListener("load", ...).

Using GSAP with React

Use the official @gsap/react hook instead of raw useEffect — it handles cleanup (reverting all animations/ScrollTriggers created inside it) automatically on unmount, which prevents duplicate/ghost triggers on re-render or React StrictMode double-invocation.

npm install @gsap/react
import { useRef } from "react";
import { gsap } from "gsap";
import { useGSAP } from "@gsap/react";

function Box() {
const container = useRef();

useGSAP(
() => {
// gsap.context() scoping happens automatically here —
// selectors like ".box" only match inside `container`
gsap.to(".box", { rotation: 360, duration: 2, repeat: -1 });
},
{ scope: container } // scopes selectors + auto-cleanup on unmount
);

return (
<div ref={container}>
<div className="box" />
</div>
);
}

useGSAP accepts a dependency array as a second option too: useGSAP(() => {...}, { dependencies: [someState], scope: container }).

Other plugins worth knowing

PluginUse case
ScrollTriggerScroll-linked animation & pinning
SplitTextSplit text into chars/words/lines to animate individually
DraggableDrag-to-interact elements (works with inertia)
FlipAnimate layout changes (FLIP technique) smoothly — great for reordering lists/grids
MotionPathPluginAnimate elements along an SVG path
ScrollSmootherButtery smooth-scroll wrapper that pairs with ScrollTrigger
TextPluginTypewriter-style text swapping

Gotchas / things I learned the hard way

  • Register plugins before usegsap.registerPlugin(ScrollTrigger) or nothing fires, silently.
  • gsap.from uses current CSS as the end state — if the element's final CSS isn't already correct in your stylesheet, from will animate to the wrong place.
  • Transforms stack per-element, not per-property — animating x and rotation together works fine (GSAP manages the transform string for you), but mixing GSAP transforms with manual CSS transform in the same stylesheet rule will conflict.
  • Kill or revert tweens on cleanup — in vanilla JS/SPA routers, call tween.kill() / ScrollTrigger.getAll().forEach(t => t.kill()) when a component/page unmounts, or you get memory leaks and stacked triggers. In React, useGSAP's auto-cleanup handles this for you.
  • will-change / layout thrashing — for heavy scroll-scrubbed animations, animating x/y/scale/opacity/rotation is cheap; animating width, height, top, left is expensive and can cause jank.
  • immediateRendergsap.from() renders the "from" state immediately by default, even before the tween starts (e.g. before a ScrollTrigger is in view). Set immediateRender: false if you don't want that flash.
  • StrictMode double effects — raw useEffect GSAP setups often run twice in dev, duplicating ScrollTriggers. useGSAP avoids this.
  • z-index / 3D transforms — animating rotationY/rotationX can trigger unexpected stacking context issues; add transformPerspective and backfaceVisibility: "hidden" for cleaner 3D flips.

Quick reference snippets

Fade + slide in on scroll:

gsap.from(".reveal", {
opacity: 0,
y: 60,
duration: 0.8,
ease: "power3.out",
scrollTrigger: { trigger: ".reveal", start: "top 85%" },
});

Looping pulse:

gsap.to(".pulse", {
scale: 1.1,
duration: 0.8,
ease: "sine.inOut",
repeat: -1,
yoyo: true,
});

Sequenced hero intro:

gsap
.timeline()
.from(".hero-title", { opacity: 0, y: 30, duration: 0.6 })
.from(".hero-subtitle", { opacity: 0, y: 20, duration: 0.5 }, "-=0.3")
.from(".hero-cta", { opacity: 0, scale: 0.8, duration: 0.4 }, "-=0.2");

Further reading