gsap-scrolltrigger
GSAP ScrollTrigger
When to use this
- An animation should play, reverse, or scrub in direct proportion to scroll position rather than on a fixed timer.
- You need a section to "pin" (stay fixed in the viewport) while content animates or scrolls past it underneath, a common pattern for step-by-step scrollytelling.
- You need scroll-position-based class/state toggling (enter/leave/enter back/leave back) without hand-rolling
IntersectionObserverplus scroll math. - You have many similar elements (cards, list rows) that should each animate independently as they individually enter the viewport, and want batching instead of one ScrollTrigger per element.
- Do NOT use this when the animation should run once on load or on a discrete interaction (click, hover) with no scroll dependency; use plain
gsap-core-timelinesinstead. Also avoid it for layout-position transitions between two DOM states; that isgsap-flip-layout.
Mental model
ScrollTrigger is fundamentally a scroll-position-to-progress mapper. Every ScrollTrigger instance defines a "start" and "end" point measured in the scroll container's coordinate space, and on every scroll event (and resize) it computes a progress value from 0 to 1 representing how far the current scroll position is between start and end. What happens with that 0-1 progress value depends on configuration: it can drive a tween's playhead directly (scrub), it can fire discrete callbacks at four transition boundaries (toggleActions, corresponding to entering forward, leaving forward, entering backward, leaving backward), or both simultaneously.
scrub fundamentally changes what the animation "is." Without scrub, the tween plays on its own internal clock once triggered, independent of further scroll input, like a normal GSAP tween that happens to start on scroll. With scrub: true (or a number of seconds), GSAP disconnects the tween from its own clock entirely and instead sets its progress() directly from the scroll-computed progress every scroll event, optionally smoothed by the scrub number as a lag/catch-up duration. This is why scrub animations can run backward when the user scrolls up: they are not "playing," they are a direct linear (or smoothed) function of scroll position.
pin: true works by measuring the trigger element's height and, once its start boundary is reached, switching it to position: fixed (via inline style, with a spacer element inserted to preserve document flow so nothing else jumps) for the duration of the scroll range, then releasing it back to normal flow at the end boundary. This means a pinned section's total scroll-distance footprint on the page is the pin duration you set (end), not the element's natural height; a common mistake is expecting the pin to release based on the element's own height rather than the configured end point.
The start/end syntax (start: 'top center', end: '+=500') is two space-separated tokens: [trigger element's edge] [viewport's edge] for the first form, meaning "trigger when the trigger element's top edge reaches the viewport's center." Relative forms like '+=500' or 'bottom top+=100' add a pixel offset. Because layout can change after images load, fonts swap, or content resizes, ScrollTrigger caches all measured positions and must be told to .refresh() when the page's geometry changes outside of a window resize (which it already listens for automatically).