
GSAP's ScrollTrigger ties any GSAP tween or timeline to the scroll position: it can play an animation when an element enters, scrub it back and forth with the scrollbar, or pin a section in place while a timeline runs. Since Webflow acquired GSAP, the whole library is free, ScrollTrigger and the plugins that used to be paid included, and commercial use is covered.
AI models produce ScrollTrigger code readily, and GSAP's own licensing FAQ addresses tools like ChatGPT, Cursor and Lovable generating it. What a prompt leaves unsaid, the code leaves to chance, and the weak points are well documented: scrub: true where a number belongs, a tween ease fighting the scroll, the pinned element animated directly, triggers created out of order, no cleanup in React, and positions measured before the fonts arrive. Each one is prevented by a single line in the prompt, and this guide gives those lines.
The distance budget for a scroll sequence, how many viewport heights each stage gets, is the subject of the scroll animation prompt. This page assumes you have one and covers the GSAP side: the settings, the mistakes, a prompt that works, and when to leave GSAP out.
GSAP, ScrollTrigger and the license#
GSAP ships as one npm package, gsap, at version 3.15.1 at the time of writing, published on 4 November 2026. The same package contains ScrollTrigger, ScrollSmoother, SplitText, Flip, Observer, MorphSVG, DrawSVG and ScrambleText, so nothing extra has to be installed or unlocked. React projects add @gsap/react for the useGSAP() hook.
The license page puts it in one line: "GSAP is now free for everyone, thanks to Webflow's support!" The standard license covers commercial use at no charge. The prohibited uses concern tools that let people build animations visually, without code, in a way that competes with Webflow's own animation builder; a website that uses GSAP is not one of them, and the licensing FAQ states plainly that GSAP code generated by AI tools such as ChatGPT, Cursor or Lovable is allowed.
ScrollTrigger's defaults are what a model falls back to when the prompt is silent, so it helps to know them. These come from the ScrollTrigger docs.
| Setting | Default | What to write in the prompt |
|---|---|---|
start | "top bottom", or "top top" when pinned | "top 75%" for a reveal |
end | "bottom top" | "+=240%" for a pinned sequence |
scrub | off | a number, 0.6 to 1.2: seconds to catch up |
toggleActions | "play none none none" | keep it, or use once: true |
anticipatePin | 0 | 1, which the docs call typically fine |
fastScrollEnd | off; 2500px/s when true | true |
pinSpacing | padding added (off inside flex containers) | leave it, or false for overlapping panels |
markers | off | true while building, removed before shipping |
A sequence worth one timeline#
Haymaker, a brand studio told as a boxing match, pins one stage over 7.2 screens of scroll and plays a whole round on it. The giant gold HAYMAKER at the kangaroo's shins jumps away letter by letter, a round clock counts from 3:00 to 0:00 and turns red, the film shadow-boxes and throws the punch at the lens at 3.19s for a "+10", the headline lands, three gold fight cards are dealt and flip, the lights flicker and die, and a spotlight iris opens onto the studio's creed, lit word by word over a second scrubbed film.
That is the shape of a GSAP timeline: labeled beats, overlapping tweens and a single playhead held by the scroll. Verdier shows the other shape, several pins on one page. Its film stage is pinned over 4.6 screens, then a rail of eight watches slides sideways in a pin whose height the script sets to the frame plus the travel, so it lasts exactly as long as the slide, and then an atelier photograph zooms from 1 to 1.4. In GSAP those are three ScrollTriggers, and the order they are created in decides whether the second and third start in the right place.
One stage pinned over 7.2 screens plays a whole boxing round: letters, clock, punch, cards, lights, creed.
Open the prompt →Three pins on one page: a 240-frame film, a sideways rail of eight watches, then an atelier zoom from 1 to 1.4.
Open the prompt →Both designs are specified without any animation library, a choice the section on when GSAP is the wrong tool comes back to. Written in GSAP, the start of a round like Haymaker's is one timeline on one ScrollTrigger:
const tl = gsap.timeline({
defaults: { ease: "none" },
scrollTrigger: {
trigger: ".show", start: "top top", end: "+=720%",
pin: true, scrub: 0.8, anticipatePin: 1, invalidateOnRefresh: true,
},
});
tl.addLabel("bell")
.to(".show .letter", { yPercent: -120, stagger: 0.04, duration: 0.6 }, "bell")
.to(".show .clock", { "--t": 0, duration: 1.2 }, "bell")
.addLabel("punch", ">-0.2")
.from(".show .score", { scale: 0.4, autoAlpha: 0, duration: 0.2 }, "punch")
.to(".show .card", { rotationY: 180, stagger: 0.15, duration: 0.5 }, "punch+=0.4");The mistakes AI makes with ScrollTrigger#
GSAP keeps its own list of the most common ScrollTrigger mistakes. A prompt that says nothing about them leaves each one to chance, so each row below is a mistake, why it breaks, and the line that rules it out.
| Mistake | Why it breaks | The line for the prompt |
|---|---|---|
scrub: true | progress follows the scrollbar exactly, so wheel steps jump | scrub is a number between 0.6 and 1.2 |
| Triggers on tweens inside a timeline | the timeline already controls their playhead | one ScrollTrigger per timeline, on the timeline |
| Animating the pinned element | it throws off ScrollTrigger's measurements | pin the wrapper, animate its children |
| Triggers created out of order | earlier pins move every later start | create ScrollTriggers top to bottom |
| One tween for many sections | they all animate at once | one ScrollTrigger per section, or ScrollTrigger.batch() for lists |
| Hard-coded pixel values | they are wrong after a resize | function-based values with invalidateOnRefresh: true |
scroll-behavior: smooth on html | it interferes with the refresh | no smooth scroll-behavior on the root |
| No refresh after late content | images and fonts move every position | ScrollTrigger.refresh() once they settle |
Lengthening a scrub with duration | duration changes nothing when start and end stay | lengthen end instead |
| No cleanup in React | Strict Mode runs effects twice; animations duplicate | useGSAP() with a scope |
Two rows deserve a closer look. Animating the pinned element throws off the measurements the pin relies on, so it engages or releases in the wrong place, and the ordering rule explains a second pin that starts early: its start was measured before the first pin added its spacing, so it is off by exactly that length. Neither shows up in the prompt that caused it, because nothing in "add a pinned scroll section" mentions either.
A prompt that produces a working ScrollTrigger page#
1. Name the stack and the cleanup#
In React or Next.js, the component is a client component, both plugins are registered once with gsap.registerPlugin(ScrollTrigger, useGSAP), and every animation is created inside useGSAP() with a scope ref. GSAP's React guide explains why: the hook reverts everything it created through gsap.context() when the component unmounts, and without it Strict Mode's double effects produce duplicate, conflicting animations.
2. Give the scroll map in viewport heights#
Every pinned stage gets its distance as an end such as "+=80%", and the whole sequence a total. The scroll map is the table to paste.
3. Give every tween its values and its share#
Each tween gets from and to values and a duration that is a proportion of the timeline, not seconds, with ease: "none" inside a scrubbed timeline so the scroll is the only curve. For a horizontal section driven by vertical scroll, the docs require the container animation to use ease: "none", and they note that pinning and snapping are not available on triggers that use containerAnimation.
4. Put reduced motion in gsap.matchMedia#
gsap.matchMedia() runs its setup only while a media query matches, and reverts every animation and ScrollTrigger created inside it when the query stops matching. Key it to (prefers-reduced-motion: no-preference) and the reduced branch is simply the static page.
5. Refresh once the page has settled#
Call ScrollTrigger.refresh() after document.fonts.ready, and again after any lazy image above the sequence loads.
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(ScrollTrigger, useGSAP);
export function Story() {
const root = useRef(null);
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
gsap.timeline({
defaults: { ease: "none" },
scrollTrigger: {
trigger: ".stage", start: "top top", end: "+=240%",
pin: true, scrub: 1, anticipatePin: 1, invalidateOnRefresh: true,
},
})
.to(".stage .product", { rotationY: 36, scale: 1.12, duration: 0.5 })
.to(".stage .caption", { autoAlpha: 0, y: -24, duration: 0.2 }, "<0.3")
.fromTo(".stage .detail", { autoAlpha: 0, y: 24 }, { autoAlpha: 1, y: 0, duration: 0.3 });
});
document.fonts.ready.then(() => ScrollTrigger.refresh());
}, { scope: root });
return <section ref={root}>{/* .stage holds .product, .caption, .detail */}</section>;
}Scroll: GSAP 3 with ScrollTrigger, registered once, all code inside
useGSAP() with a scope ref. One pinned sequence: pin the .stage wrapper
(never animate it), end "+=240%", scrub 1, anticipatePin 1,
invalidateOnRefresh true, ease "none" on every tween. Durations are
shares of the timeline: product turn 0.5, caption out 0.2, detail in
0.3. Create triggers in page order. Wrap everything in gsap.matchMedia
on (prefers-reduced-motion: no-preference). No scroll-behavior: smooth
on html. ScrollTrigger.refresh() after document.fonts.ready.That fragment was written for this guide. If you work in Claude Code, the Claude Code UI prompts cover the rest of the component, and a 3D scene inside the pin needs the camera and damping values in the 3D website guide.
Smooth scrolling: ScrollSmoother, Lenis or neither#
A numeric scrub already smooths the animation. A smooth-scrolling layer smooths the scroll itself, and the two stack, so if you add one, lower the scrub.
- ScrollSmoother is GSAP's own, free like the rest. It needs a
#smooth-wrapperaround a#smooth-contentelement, itssmoothvalue is the seconds it takes to catch up to the native position (0.8by default), it uses native scrolling with no fake scrollbar, and it does not smooth touch-only devices unless asked. Witheffects: trueit readsdata-speedanddata-lagattributes, sodata-speed="0.5"scrolls an element at half speed (ScrollSmoother docs). Fixed elements have to sit outside the wrapper, because the content is moved with a transform. - Lenis, an MIT-licensed smooth-scroll library, is the other common choice; the three lines that keep it in step with ScrollTrigger are in the ScrollTrigger settings.
- Neither is often right. Native scroll with
scrub: 1feels smooth on a trackpad, keeps keyboard and find-in-page behavior intact, and is one less thing to tune on phones.
The data-speed attribute is also the quickest route to a parallax layer in a GSAP project, which the parallax examples compare with the CSS and hand-written versions.
When GSAP is the wrong tool#
The scroll-driven designs on this page are all specified without an animation library: Ardente, Verdier, Haymaker, Lacca and Palenque pin with position: sticky where they pin at all, read scrollY against measurements taken on load, when the fonts are ready and on resize, and drive everything from one requestAnimationFrame loop over the browser's native scroll. It keeps the page light, with no library to download or parse, and for their main effect, a scrubbed film, it is also the simpler tool: a film needs a guarded seek (seek only when the target is 20ms off, never while a seek is in flight), which takes a few lines in a loop and fights a tween on currentTime that writes the time on every tick.
| The job | Without a library | Reach for GSAP |
|---|---|---|
| A reveal on entry | IntersectionObserver or a CSS view timeline | only if it is part of a sequence |
| A progress bar | animation-timeline: scroll(root) | no |
| A scrubbed film | sticky pin, one loop, guarded seeks | no |
| Many overlapping tweens with labels | possible, but you write the timeline | yes, a scrubbed timeline |
| Snapping between sections | CSS scroll snap | yes, snap |
| SVG drawing and morphing, split text | hard | yes, DrawSVG, MorphSVG, SplitText |
| Layout changes animated between states | hard | yes, Flip |
CSS scroll-driven animations, Baseline since Firefox 159 shipped them in October 2026, now cover the first two rows in every major engine, which the scroll animation examples walk through.
Build the sequence, then check it#
Write the scroll map, name the settings in the table, add the ten prevention lines, and generate. Then check three things in the browser with markers: true: the pin engages without a jump, the second pin starts where the first ends, and a window resize puts every marker back in place. For a long editorial page built from several such sequences, the scrollytelling guide covers the structure, and the rest of this cluster is on the build with AI hub.
Open Haymaker, watch the round play in its preview, and copy its prompt into Claude Code or Cursor: every beat of the pinned show is already timed against the scroll, and the hosted films it uses are listed. Keep it library-free as written, or ask the model to express the same beats as one GSAP timeline if your project already ships GSAP. The Thanor MCP, included in every paid plan, lets Claude, ChatGPT or Cursor pull a design's full art direction without the clipboard. Plans are on the pricing page, Vesperion is free with an account, and the rest of the library is open to browse.
Questions this raises
Is GSAP free for commercial use?
Yes. GSAP's standard license, owned by Webflow, covers commercial use at no charge, and that includes the plugins that used to be paid, such as SplitText and MorphSVG. The restriction that matters is building a visual, no-code animation tool that competes with Webflow's own. GSAP's licensing FAQ also states that code written by AI tools is allowed.
What does scrub do in GSAP ScrollTrigger?
Scrub links an animation's progress to the scroll position, so it plays forward as you scroll down and backward as you scroll up. scrub true links them directly, so every wheel step lands as a jump. A number gives the playhead that many seconds to catch up: scrub 1 trails the scroll by about a second and reads as a smooth camera move.
Can ChatGPT or Claude write GSAP ScrollTrigger code?
Yes, and GSAP's licensing FAQ explicitly allows code generated by tools like ChatGPT and Cursor. The output is only as good as the prompt. Leave the settings unsaid and the code can fall into the mistakes GSAP documents: scrub set to true, eases inside scrubbed timelines, the pinned element animated directly, triggers out of order, no cleanup in React. Name the values and the rules, and they are ruled out.
Why does my ScrollTrigger break after resizing the window?
Three causes cover most cases. scroll-behavior smooth on the html element interferes with ScrollTrigger's refresh, so remove it. Values that depend on the viewport were computed once, so make them functions and set invalidateOnRefresh to true. And content that loaded late moved everything, so call ScrollTrigger.refresh after fonts and images settle.
Part of
Build it with AI
Three dimensional scenes, scroll driven motion and animated pages, built by handing a model a specification instead of an adjective. What to write, and what comes back.
