Imported from ilyvsc/ado.fan (
.claude/skills/gsap-scrolltrigger/SKILL.md). Install upstream withnpx skills add ilyvsc/ado.fan --skill gsap-scrolltrigger. Copyright stays with the author.
GSAP ScrollTrigger
Scroll-driven animations and interactions.
Quick Start
bun install gsap
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
gsap.to(".box", {
x: 500,
scrollTrigger: {
trigger: ".box",
start: "top center",
end: "bottom center",
scrub: true,
},
});
Core Concepts
Basic ScrollTrigger
gsap.to(".element", {
x: 200,
scrollTrigger: {
trigger: ".element", // Element that triggers the animation
start: "top center", // When trigger hits viewport center
end: "bottom center", // When trigger leaves viewport center
toggleActions: "play pause resume reset",
},
});
Start/End Positions
// Format: "trigger-position viewport-position"
start: "top center"; // Trigger's top hits viewport center
start: "top 80%"; // Trigger's top hits 80% down viewport
start: "center center"; // Trigger's center hits viewport center
start: "bottom top"; // Trigger's bottom hits viewport top
start: "top top+=100"; // Trigger's top hits 100px below viewport top
Position Reference
| Value | Description |
|---|---|
top |
Top edge |
center |
Center |
bottom |
Bottom edge |
80% |
80% from top |
+=100 |
Plus 100 pixels |
-=50 |
Minus 50 pixels |
Scrub Animations
Basic Scrub
// Animation progress tied to scroll position
gsap.to(".progress-bar", {
scaleX: 1,
scrollTrigger: {
trigger: ".content",
start: "top top",
end: "bottom bottom",
scrub: true, // Directly linked to scroll
},
});
Smooth Scrub
gsap.to(".element", {
x: 500,
scrollTrigger: {
trigger: ".section",
scrub: 1, // 1 second smoothing
// scrub: 0.5 // 0.5 second smoothing
// scrub: 2 // 2 second smoothing (laggy feel)
},
});
Scrub with Timeline
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
start: "top top",
end: "+=3000", // Scroll distance
scrub: 1,
pin: true,
},
});
tl.to(".step1", { opacity: 1 })
.to(".step2", { opacity: 1 })
.to(".step3", { opacity: 1 });
Pinning
Basic Pin
ScrollTrigger.create({
trigger: ".panel",
start: "top top",
end: "+=500", // Pin for 500px of scroll
pin: true,
});
Pin with Animation
gsap.to(".content", {
x: "-200%",
ease: "none",
scrollTrigger: {
trigger: ".horizontal-section",
start: "top top",
end: () => "+=" + document.querySelector(".horizontal-section").offsetWidth,
pin: true,
scrub: 1,
},
});
Pin Spacing
ScrollTrigger.create({
trigger: ".section",
pin: true,
pinSpacing: true, // Default: adds space for pinned duration
// pinSpacing: false // No extra space (content overlaps)
// pinSpacing: '500px' // Custom spacing
});
Toggle Actions
Action Syntax
// Format: "onEnter onLeave onEnterBack onLeaveBack"
toggleActions: "play pause resume reset";
// Common combinations:
toggleActions: "play none none none"; // Play once
toggleActions: "play reverse play reverse"; // Toggle direction
toggleActions: "restart none none none"; // Restart each time
toggleActions: "play complete reverse reset";
Action Values
| Action | Effect |
|---|---|
play |
Play forward |
pause |
Pause |
resume |
Resume from paused |
reverse |
Play backward |
restart |
Restart from beginning |
reset |
Reset to start (no animation) |
complete |
Jump to end |
none |
Do nothing |
Snap Points
Basic Snap
ScrollTrigger.create({
trigger: ".sections",
start: "top top",
end: "bottom bottom",
snap: 1 / 4, // Snap to quarters
});
Snap to Labels
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
scrub: 1,
snap: {
snapTo: "labels",
duration: 0.5,
ease: "power2.inOut",
},
},
});
tl.addLabel("intro")
.to(".a", { opacity: 1 })
.addLabel("middle")
.to(".b", { opacity: 1 })
.addLabel("end");
Snap Configuration
snap: {
snapTo: [0, 0.25, 0.5, 0.75, 1], // Snap to specific points
duration: { min: 0.2, max: 0.6 }, // Snap duration range
delay: 0, // Delay before snap
ease: 'power1.inOut', // Snap easing
directional: true // Snap in scroll direction
}
Callbacks
ScrollTrigger Callbacks
ScrollTrigger.create({
trigger: ".section",
onEnter: () => console.log("Entered"),
onLeave: () => console.log("Left"),
onEnterBack: () => console.log("Entered from bottom"),
onLeaveBack: () => console.log("Left going up"),
onUpdate: (self) => console.log("Progress:", self.progress),
onToggle: (self) => console.log("Active:", self.isActive),
onRefresh: () => console.log("Refreshed"),
});
Progress-Based Updates
ScrollTrigger.create({
trigger: ".section",
start: "top bottom",
end: "bottom top",
onUpdate: (self) => {
// self.progress: 0 to 1
// self.direction: 1 (down) or -1 (up)
// self.velocity: scroll speed
updateElement(self.progress);
},
});
Parallax Effects
Basic Parallax
// Background moves slower than scroll
gsap.to(".background", {
yPercent: -50,
ease: "none",
scrollTrigger: {
trigger: ".section",
scrub: true,
},
});
// Foreground moves faster
gsap.to(".foreground", {
yPercent: 50,
ease: "none",
scrollTrigger: {
trigger: ".section",
scrub: true,
},
});
Multi-Layer Parallax
const layers = [
{ selector: ".layer-1", speed: -20 },
{ selector: ".layer-2", speed: -40 },
{ selector: ".layer-3", speed: -60 },
{ selector: ".layer-4", speed: -80 },
];
layers.forEach((layer) => {
gsap.to(layer.selector, {
yPercent: layer.speed,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true,
},
});
});
Horizontal Scrolling
Horizontal Section
const sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".horizontal-container",
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1),
end: () =>
"+=" + document.querySelector(".horizontal-container").offsetWidth,
},
});
Markers (Development)
ScrollTrigger.create({
trigger: ".section",
start: "top center",
end: "bottom center",
markers: true, // Show visual markers
// markers: { startColor: 'green', endColor: 'red', fontSize: '12px' }
});
Batch Animations
Stagger on Scroll
ScrollTrigger.batch(".card", {
onEnter: (elements) => {
gsap.from(elements, {
opacity: 0,
y: 50,
stagger: 0.1,
duration: 0.5,
});
},
start: "top 85%",
});
Batch Configuration
ScrollTrigger.batch(".item", {
interval: 0.1, // Time between batches
batchMax: 3, // Max items per batch
onEnter: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.1 }),
onLeave: (batch) => gsap.to(batch, { opacity: 0, y: 20 }),
onEnterBack: (batch) => gsap.to(batch, { opacity: 1, y: 0 }),
onLeaveBack: (batch) => gsap.to(batch, { opacity: 0, y: -20 }),
});
Common Patterns
Reveal on Scroll
gsap.utils.toArray(".reveal").forEach((elem) => {
gsap.from(elem, {
opacity: 0,
y: 50,
duration: 0.8,
scrollTrigger: {
trigger: elem,
start: "top 80%",
toggleActions: "play none none none",
},
});
});
Progress Indicator
gsap.to(".progress-bar", {
scaleX: 1,
transformOrigin: "left center",
ease: "none",
scrollTrigger: {
trigger: "body",
start: "top top",
end: "bottom bottom",
scrub: 0.3,
},
});
Sticky Header Transform
ScrollTrigger.create({
start: "top -80",
onUpdate: (self) => {
if (self.direction === 1) {
gsap.to(".header", { y: -80, duration: 0.3 });
} else {
gsap.to(".header", { y: 0, duration: 0.3 });
}
},
});
Temporal Collapse Patterns
Countdown Scroll Reveal
// Reveal countdown sections as user scrolls
const sections = ["days", "hours", "minutes", "seconds"];
sections.forEach((section, i) => {
gsap.from(`.countdown-${section}`, {
opacity: 0,
scale: 0.8,
y: 50,
duration: 0.8,
ease: "power3.out",
scrollTrigger: {
trigger: `.countdown-${section}`,
start: "top 80%",
toggleActions: "play none none none",
},
});
});
Scroll-Based Time Dilation Effect
gsap
.timeline({
scrollTrigger: {
trigger: ".time-section",
start: "top center",
end: "bottom center",
scrub: 1,
},
})
.to(".time-digit", {
textShadow: "0 0 50px #FFFFFF, 0 0 100px #FFFFFF",
scale: 1.1,
})
.to(
".particles",
{
opacity: 1,
filter: "blur(0px)",
},
"<",
);
Performance Tips
// Disable on mobile if needed
ScrollTrigger.matchMedia({
'(min-width: 768px)': function() {
// Desktop animations
},
'(max-width: 767px)': function() {
// Simpler mobile animations
}
});
// Refresh on resize
ScrollTrigger.refresh();
// Kill all ScrollTriggers
ScrollTrigger.killAll();
// Kill specific trigger
const st = ScrollTrigger.create({ ... });
st.kill();