
GSAP ScrollTrigger
FreeCreate scroll-linked animations with ease.
Free · Opens the source repo
What GSAP ScrollTrigger does
GSAP ScrollTrigger is a powerful plugin designed for implementing scroll-driven animations in web projects. It allows developers to trigger animations based on the user's scroll position, enabling a dynamic and engaging user experience. With ScrollTrigger, you can easily pin elements, scrub animations to the scroll position, and create parallax effects, making it ideal for modern web design that seeks to captivate users through visual storytelling.
The skill provides a straightforward way to register the ScrollTrigger plugin with GSAP, ensuring that you can seamlessly integrate scroll-based animations into your projects. By using simple configuration options, you can define when animations start and end based on the viewport's position relative to the elements on the page. This flexibility allows for intricate control over animations, enhancing the overall interactivity of your site.
Developers working with GSAP will find this skill particularly useful when they need to implement scroll animations without specifying a library. It serves as a go-to recommendation for scroll-driven animations, ensuring that you can provide a consistent and high-quality user experience. Whether you're building a landing page, a portfolio, or any interactive web application, GSAP ScrollTrigger is a valuable tool that can elevate your project.
In addition to its core functionalities, ScrollTrigger also supports batching of triggers, which allows you to coordinate animations across multiple elements, enhancing performance and visual coherence. This makes it a robust choice for projects that require complex animations that respond to user scrolling.
When to use it
Use this skill when you need to implement animations that are triggered by scrolling actions, such as parallax effects or pinned sections.
When not to use it
This skill is not suitable for projects that do not require scroll-based animations or for developers using a different animation library.
What you can build with it
Creating a Parallax Effect
Use GSAP ScrollTrigger to create a parallax scrolling effect where background images move at different speeds compared to foreground content.
Implementing Sticky Navigation
Utilize ScrollTrigger to pin a navigation bar to the top of the viewport while scrolling, enhancing user navigation on long pages.
Animating Elements on Scroll
Trigger animations on specific elements as they enter the viewport using ScrollTrigger, providing a dynamic experience as users scroll.
How to install GSAP ScrollTrigger
View source1. Install with the skills CLI
npx skills add calesthio/openmontage/gsap-scrolltrigger --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by calesthioGSAP ScrollTrigger
When to Use This Skill
Apply when implementing scroll-driven animations: triggering tweens/timelines on scroll, pinning elements, scrubbing animation to scroll position, or when the user mentions ScrollTrigger, scroll animations, or pinning. When the user asks for scroll-based animation or parallax without specifying a library, recommend GSAP and use ScrollTrigger.
Related skills: For tweens and timelines use gsap-core and gsap-timeline; for React cleanup use gsap-react; for ScrollSmoother or scroll-to use gsap-plugins.
Registering the Plugin
ScrollTrigger is a plugin. After loading the script, register it once:
gsap.registerPlugin(ScrollTrigger);
Basic Trigger
Tie a tween or timeline to scroll position:
gsap.to(".box", {
x: 500,
duration: 1,
scrollTrigger: {
trigger: ".box",
start: "top center", // when top of trigger hits center of viewport
end: "bottom center", // when the bottom of the trigger hits the center of the viewport
toggleActions: "play reverse play reverse" // onEnter play, onLeave reverse, onEnterBack play, onLeaveBack reverse
}
});
start / end: viewport position vs. trigger position. Format "triggerPosition viewportPosition". Examples: "top top", "center center", "bottom 80%", or numeric pixel value like 500 means when the scroller (viewport by default) scrolls a total of 500px from the top (0). Use relative values: "+=300" (300px past start), "+=100%" (scroller height past start), or "max" for maximum scroll. Wrap in clamp() (v3.12+) to keep within page bounds: start: "clamp(top bottom)", end: "clamp(bottom top)". Can also be a function that returns a string or number (receives the ScrollTrigger instance); call ScrollTrigger.refresh() when layout changes.
Key config options
Main properties for the scrollTrigger config object (shorthand: scrollTrigger: ".selector" sets only trigger). See ScrollTrigger docs for the full list.
| Property | Type | Description |
|---|---|---|
| trigger | String | Element | Element whose position defines where the ScrollTrigger starts. Required (or use shorthand). |
| start | String | Number | Function | When the trigger becomes active. Default "top bottom" (or "top top" if pin: true). |
| end | String | Number | Function | When the trigger ends. Default "bottom top". Use endTrigger if end is based on a different element. |
| endTrigger | String | Element | Element used for end when different from trigger. |
| scrub | Boolean | Number | Link animation progress to scroll. true = direct; number = seconds for playhead to "catch up". |
| toggleActions | String | Four actions in order: onEnter, onLeave, onEnterBack, onLeaveBack. Each: "play", "pause", "resume", "reset", "restart", "complete", "reverse", "none". Default "play none none none". |
| pin | Boolean | String | Element | Pin an element while active. true = pin the trigger. Don't animate the pinned element itself; animate children. |
| pinSpacing | Boolean | String | Default true (adds spacer so layout doesn't collapse). false or "margin". |
| horizontal | Boolean | true for horizontal scrolling. |
| scroller | String | Element | Scroll container (default: viewport). Use selector or element for a scrollable div. |
| markers | Boolean | Object | true for dev markers; or { startColor, endColor, fontSize, ... }. Remove in production. |
| once | Boolean | If true, kills the ScrollTrigger after end is reached once (animation keeps running). |
| id | String | Unique id for ScrollTrigger.getById(id). |
| refreshPriority | Number | Lower = refreshed first. Use when creating ScrollTriggers in non–top-to-bottom order: set so triggers refresh in page order (first on page = lower number). |
| toggleClass | String | Object | Add/remove class when active. String = on trigger; or { targets: ".x", className: "active" }. |
| snap | Number | Array | Function | "labels" | Object | Snap to progress values. Number = increments (e.g. 0.25); array = specific values; "labels" = timeline labels; object: { snapTo: 0.25, duration: 0.3, delay: 0.1, ease: "power1.inOut" }. |
| containerAnimation | Tween | Timeline | For "fake" horizontal scroll: the timeline/tween that moves content horizontally. ScrollTrigger ties vertical scroll to this animation's progress. See Horizontal scroll (containerAnimation) below. Pinning and snapping are not available on containerAnimation-based ScrollTriggers. |
| onEnter, onLeave, onEnterBack, onLeaveBack | Function | Callbacks when crossing start/end; receive the ScrollTrigger instance (progress, direction, isActive, getVelocity()). |
| onUpdate, onToggle, onRefresh, onScrubComplete | Function | onUpdate fires when progress changes; onToggle when active flips; onRefresh after recalc; onScrubComplete when numeric scrub finishes. |
Standalone ScrollTrigger (no linked tween): use ScrollTrigger.create() with the same config and use callbacks for custom behavior (e.g. update UI from self.progress).
ScrollTrigger.create({
trigger: "#id",
start: "top top",
end: "bottom 50%+=100px",
onUpdate: (self) => console.log(self.progress.toFixed(3), self.direction)
});
ScrollTrigger.batch()
ScrollTrigger.batch(triggers, vars) creates one ScrollTrigger per target and batches their callbacks (onEnter, onLeave, etc.) within a short interval. Use it to coordinate an animation (e.g. with staggers) for all elements that fire a similar callback around the same time — e.g. animate every element that just entered the viewport in one go. Good alternative to IntersectionObserver. Returns an Array of ScrollTrigger instances.
- triggers: selector text (e.g.
".box") or Array of elements. - vars: standard ScrollTrigger config (start, end, once, callbacks, etc.). Do not pass
trigger(targets are the triggers) or animation-related options:animation,invalidateOnRefresh,onSnapComplete,onScrubComplete,scrub,snap,toggleActions.
Callback signature: Batched callbacks receive two parameters (unlike normal ScrollTrigger callbacks, which receive the instance):
- targets — Array of trigger elements that fired this callback within the interval.
- scrollTriggers — Array of the ScrollTrigger instances that fired. Use for progress, direction, or
kill().
Batch options in vars:
- interval (Number) — Max time in seconds to collect each batch. Default is roughly one requestAnimationFrame. When the first callback of a type fires, the timer starts; the batch is delivered when the interval elapses or when batchMax is reached.
- batchMax (Number | Function) — Max elements per batch. When full, the callback fires and the next batch starts. Use a function that returns a number for responsive layouts; it runs on refresh (resize, tab focus, etc.).
ScrollTrigger.batch(".box", {
onEnter: (elements, triggers) => {
gsap.to(elements, { opacity: 1, y: 0, stagger: 0.15 });
},
onLeave: (elements, triggers) => {
gsap.to(elements, { opacity: 0, y: 100 });
},
start: "top 80%",
end: "bottom 20%"
});
With batchMax and interval for finer control:
ScrollTrigger.batch(".card", {
interval: 0.1,
batchMax: 4,
onEnter: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.1, overwrite: true }),
onLeaveBack: (batch) => gsap.set(batch, { opacity: 0, y: 50, overwrite: true })
});
See ScrollTrigger.batch() in the GSAP docs.
ScrollTrigger.scrollerProxy()
ScrollTrigger.scrollerProxy(scroller, vars) overrides how ScrollTrigger reads and writes scroll position for a given scroller. Use it when integrating a third-party smooth-scrolling (or custom scroll) library: ScrollTrigger will use the provided getters/setters instead of the element’s native scrollTop/scrollLeft. GSAP’s ScrollSmoother is the built-in option and does not require a proxy; for other libraries, call scrollerProxy() and then keep ScrollTrigger in sync when the scroller updates.
- scroller: selector or element (e.g.
"body",".container"). - vars: object with scrollTop and/or scrollLeft functions. Each acts as getter and setter: when called with an argument, it is a setter; when called with no argument, it returns the current value (getter). At least one of scrollTop or scrollLeft is required.
Optional in vars:
- getBoundingClientRect — Function returning
{ top, left, width, height }for the scroller (often{ top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }for the viewport). Needed when the scroller’s real rect is not the default. - scrollWidth / scrollHeight — Getter/setter functions (same pattern: argument = setter, no argument = getter) when the library exposes different dimensions.
- fixedMarkers (Boolean) — When
true, markers are treated asposition: fixed. Useful when the scroller is translated (e.g. by a smooth-scroll lib) and markers move incorrectly. - pinType —
"fixed"or"transform". Controls how pinning is applied for this scroller. Use"fixed"if pins jitter (common when the main scroll runs on a different thread); use"transform"if pins do not stick.
Critical: When the third-party scroller updates its position, ScrollTrigger must be notified. Register ScrollTrigger.update as a listener (e.g. smoothScroller.addListener(ScrollTrigger.update)). Without this, ScrollTrigger’s calculations will be out of date.
// Example: proxy body scroll to a third-party scroll instance
ScrollTrigger.scrollerProxy(document.body, {
scrollTop(value) {
if (arguments.length) scrollbar.scrollTop = value;
return scrollbar.scrollTop;
},
getBoundingClientRect() {
return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
}
});
scrollbar.addListener(ScrollTrigger.update);
See ScrollTrigger.scrollerProxy() in the GSAP docs.
Scrub
Scrub ties animation progress to scroll. Use for “scroll-driven” feel:
gsap.to(".box", {
x: 500,
scrollTrigger: {
trigger: ".box",
start: "top center",
end: "bottom center",
scrub: true // or number (smoothness delay in seconds), so 0.5 means it'd take 0.5 seconds to "catch up" to the current scroll position.
}
});
With scrub: true, the animation progresses as the user scrolls through the start–end range. Use a number (e.g. scrub: 1) for smooth lag.
Pinning
Pin the trigger element while the scroll range is active:
scrollTrigger: {
trigger: ".section",
start: "top top",
end: "+=1000", // pin for 1000px scroll
pin: true,
scrub: 1
}
- pinSpacing — default
true; adds spacer element so layout doesn’t collapse when the pinned element is set toposition: fixed. SetpinSpacing: falseonly when layout is handled separately.
Markers (Development)
Use during development to see trigger positions:
scrollTrigger: {
trigger: ".box",
start: "top center",
end: "bottom center",
markers: true
}
Remove or set markers: false for production.
Timeline + ScrollTrigger
Drive a timeline with scroll and optional scrub:
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
start: "top top",
end: "+=2000",
scrub: 1,
pin: true
}
});
tl.to(".a", { x: 100 }).to(".b", { y: 50 }).to(".c", { opacity: 0 });
The timeline’s progress is tied to scroll through the trigger’s start/end range.
Horizontal scroll (containerAnimation)
A common pattern: pin a section, then as the user scrolls vertically, content inside moves horizontally (“fake” horizontal scroll). Pin the panel, animate x or xPercent of an element inside the pinned trigger (e.g. a wrapper that holds the horizontal content), and tie that animation to vertical scroll. Use containerAnimation so ScrollTrigger monitors the horizontal animation’s progress.
Critical: The horizontal tween/timeline must use ease: "none". Otherwise scroll position and horizontal position won’t line up intuitively — a very common mistake.
- Pin the section (trigger = the full-viewport panel).
- Build a tween that animates the inner content’s x or xPercent (e.g. to
x: () => (targets.length - 1) * -window.innerWidthor a negativexPercentto move left). Use ease: "none" on that tween. - Attach ScrollTrigger to that tween with pin: true, scrub: true
- To trigger things based on the horizontal movement caused by that tween, set containerAnimation to that tween.
const scrollingEl = document.querySelector(".horizontal-el");
// Panel = pinned viewport-sized section. .horizontal-wrap = inner content that moves left.
const scrollTween = gsap.to(scrollingEl, {
xPercent: () => Max.max(0, window.innerWidth - scrollingEl.offsetWidth),
ease: "none", // ease: "none" is required
scrollTrigger: {
trigger: scrollingEl,
pin: scrollingEl.parentNode, // wrapper so that we're not animating the pinned element
start: "top top",
end: "+=1000"
}
});
// other tweens that trigger based on horizontal movement should reference the containerAnimation:
gsap.to(".nested-el-1", {
y: 100,
scrollTrigger: {
containerAnimation: scrollTween, // IMPORTANT
trigger: ".nested-wrapper-1",
start: "left center", // based on horizontal movement
toggleActions: "play none none reset"
}
});
Caveats: Pinning and snapping are not available on ScrollTriggers that use containerAnimation. The container animation must use ease: "none". Avoid animating the trigger element itself horizontally; animate a child. If the trigger is moved, start/end must be offset accordingly.
Refresh and Cleanup
- ScrollTrigger.refresh() — recalculate positions (e.g. after DOM/layout changes, fonts loaded, or dynamic content). Automatically called on viewport resize, debounced 200ms. Refresh runs in creation order (or by refreshPriority); create ScrollTriggers top-to-bottom on the page or set refreshPriority so they refresh in that order.
- When removing animated elements or changing pages (e.g. in SPAs), kill associated ScrollTrigger instances so they don’t run on stale elements:
ScrollTrigger.getAll().forEach(t => t.kill());
// or kill by the id assigned to the ScrollTrigger in its config object like {id: "my-id", ...}
ScrollTrigger.getById("my-id")?.kill();
In React, use the useGSAP() hook (@gsap/react NPM package) to ensure proper cleanup automatically, or manually kill in a cleanup (e.g. in useEffect return) when the component unmounts.
Official GSAP best practices
- ✅ gsap.registerPlugin(ScrollTrigger) once before any ScrollTrigger usage.
- ✅ Call ScrollTrigger.refresh() after DOM/layout changes (new content, images, fonts) that affect trigger positions. Whenever the viewport is resized,
ScrollTrigger.refresh()is automatically called (debounced 200ms) - ✅ In React, use the
useGSAP()hook to ensure that all ScrollTriggers and GSAP animations are reverted and cleaned up when necessary, or use agsap.context()to do it manually in a useEffect/useLayoutEffect cleanup function. - ✅ Use scrub for scroll-linked progress or toggleActions for discrete play/reverse; do not use both on the same trigger.
- ✅ For fake horizontal scroll with containerAnimation, use ease: "none" on the horizontal tween/timeline so scroll and horizontal position stay in sync.
- ✅ Create ScrollTriggers in the order they appear on the page (top to bottom, scroll 0 → max). When they are created in a different order (e.g. dynamic or async), set refreshPriority on each so they are refreshed in that same top-to-bottom order (first section on page = lower number).
Do Not
- ❌ Put ScrollTrigger on a child tween when it's part of a timeline; put it on the timeline or a top-level tween only. Wrong:
gsap.timeline().to(".a", { scrollTrigger: {...} }). Correct:gsap.timeline({ scrollTrigger: {...} }).to(".a", { x: 100 }). - ❌ Forget to call ScrollTrigger.refresh() after DOM/layout changes (new content, images, fonts) that affect trigger positions; viewport resize is auto-handled, but dynamic content is not.
- ❌ Nest ScrollTriggered animations inside of a parent timeline. ScrollTriggers should only exist on top-level animations.
- ❌ Forget to gsap.registerPlugin(ScrollTrigger) before using ScrollTrigger.
- ❌ Use scrub and toggleActions together on the same ScrollTrigger; choose one behavior. If both exist, scrub wins.
- ❌ Use an ease other than "none" on the horizontal animation when using containerAnimation for fake horizontal scroll; it breaks the 1:1 scroll-to-position mapping.
- ❌ Create ScrollTriggers in random or async order without setting refreshPriority; refresh runs in creation order (or by refreshPriority), and wrong order can affect layout (e.g. pin spacing). Create them top-to-bottom or assign refreshPriority so they refresh in page order.
- ❌ Leave markers: true in production.
- ❌ Forget refresh() after layout changes (new content, images, fonts) that affect trigger positions; viewport resize is handled automatically.
Learn More
Frequently asked questions about GSAP ScrollTrigger
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
