Access the AI Animation Generator and premium effects
Upgrade your plan to keep generating animations and unlock premium effects.
Master scroll-driven animations and build interactive scroll stories. Learn to trigger animations on viewport entry, scrub with scroll, pin elements, and create sophisticated parallax effects. Complete with interactive demos and real-world patterns.
ScrollTrigger is a powerful GSAP plugin that binds animations to scroll position. Instead of animations playing when the page loads, they trigger when elements enter the viewport or at specific scroll positions. This unlocks scroll-driven animations, parallax effects, scroll stories, and interactive scroll experiences—all with the performance and control of GSAP.
At its core, ScrollTrigger watches scroll position and fires animations when triggers are met. It handles all the complex viewport calculations, performance optimization, and browser quirks automatically. You define when animations should trigger (start and end positions), what animations play, and whether they scrub with scroll or play at specific moments.
Scroll to trigger
// Basic ScrollTrigger animation
gsap.to('.box', {
duration: 1,
opacity: 1,
y: 0,
scrollTrigger: {
trigger: '.box', // Element that triggers animation
start: 'top 80%', // When top of box hits 80% of viewport
end: 'top 50%', // When top of box hits 50% of viewport
toggleActions: 'play none none none' // Play on enter only
}
});
// Register ScrollTrigger plugin (required)
gsap.registerPlugin(ScrollTrigger);
// Detect when trigger fires
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
onEnter: () => console.log('Entered viewport'),
onLeave: () => console.log('Left viewport')
}
});
ScrollTrigger is the foundation for modern scroll animations. Every time you want animations to respond to scroll—entrance effects, parallax, scroll stories, interactive experiences—ScrollTrigger is your tool. Master it and unlock sophisticated scroll experiences.
Getting ScrollTrigger up and running is straightforward. You have multiple installation options depending on your project setup: CDN for quick testing, NPM/Yarn for modern projects, or native Webflow integration for no-code setup.
<!-- Add before closing body tag -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger);
// Your animations here
gsap.to(".box", {
scrollTrigger: ".box",
x: 500,
duration: 2
});
</script>
# Install GSAP
npm install gsap
# Or with Yarn
yarn add gsap
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// Register the plugin
gsap.registerPlugin(ScrollTrigger);
// Your animations here
gsap.to(".box", {
scrollTrigger: ".box",
x: 500
});
As of 2025, GSAP and ScrollTrigger are natively integrated into Webflow. Enable it in your project settings:
<script>
document.addEventListener('DOMContentLoaded', () => {
gsap.registerPlugin(ScrollTrigger);
// Your animations here
gsap.to(".hero-image", {
scrollTrigger: {
trigger: ".hero-section",
start: "top top",
end: "bottom top",
scrub: 1
},
y: 200,
scale: 1.2
});
});
</script>
gsap.registerPlugin(ScrollTrigger) before using itThe simplest way to use ScrollTrigger is to add it as a property to any GSAP animation. Define a trigger element, set start and end positions, and let ScrollTrigger handle the rest.
gsap.to(".box", {
scrollTrigger: ".box", // trigger when .box enters viewport
x: 500,
duration: 2
});
This animates the box when it enters the viewport. Once triggered, the animation plays once and completes. For more control, pass ScrollTrigger as an object with configuration options.
gsap.to(".box", {
scrollTrigger: {
trigger: ".box",
start: "top center", // when top of trigger hits center of viewport
end: "bottom top", // when bottom of trigger hits top of viewport
markers: true, // show visual markers (for debugging)
scrub: true, // link animation to scrollbar
pin: true // pin the element while scrolling
},
x: 500
});
The start and end positions define when ScrollTrigger fires. They use a simple syntax: 'triggerPosition viewportPosition'. The trigger position is relative to the trigger element (top, center, bottom), and the viewport position is a percentage or pixel value of the viewport height.
For example, 'top 80%' means "fire when the top of the trigger element hits 80% down the viewport." This gives you pixel-perfect control over when animations start and stop. Understanding start/end positions is critical for precise animation timing and scroll-driven effects.
Scroll to see different start/end positions trigger
// Start/End Position Syntax: 'triggerPosition viewportPosition'
// START POSITION EXAMPLES
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%' // Fire when box top hits 80% of viewport
}
});
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'center center' // Fire when box center hits viewport center
}
});
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'bottom top' // Fire when box bottom hits top of viewport
}
});
// END POSITION
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%' // Stop when box top hits 20% of viewport
}
});
// TRIGGER POSITION OPTIONS
// 'top' - Top of element
// 'center' - Center of element
// 'bottom' - Bottom of element
// VIEWPORT POSITION OPTIONS
// 'top' - Top of viewport
// 'center' - Center of viewport (50%)
// 'bottom' - Bottom of viewport
// Or any percentage: '75%', '25%'
// Or pixel offsets: 'top +=100', 'center -=50'
| Start Position | When It Fires | Use Case |
|---|---|---|
top 80% |
When element is 80% down viewport | Early trigger (element already visible) |
top center |
When element reaches viewport center | Moderate scroll position |
top top |
When element hits top of viewport | Late trigger (element just entered) |
center center |
When element center hits viewport center | Perfect viewport alignment |
bottom top |
When element leaves viewport | Very late trigger |
Scrubbing ties animation progress directly to scroll position. Instead of playing an animation when triggered, the animation's playhead moves with the user's scroll. This creates interactive, scroll-driven experiences where users feel in control of the animation. Scrubbing is essential for scroll stories, progress indicators, and parallax effects.
Use scrub: true for instant scrubbing with no smoothing, or scrub: 1 for 1 second of smoothing between scroll and animation position. The smoothing value creates a lag effect where the animation catches up to scroll position—useful for polished, organic feeling animations.
Scroll progress: 0%
Scroll to see scrubbing in action
// Scrubbing links animation to scroll position
// INSTANT SCRUBBING (no smoothing)
gsap.to('.box', {
duration: 2,
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
scrub: true // Instant scrubbing
}
});
// SMOOTH SCRUBBING (with lag)
gsap.to('.box', {
duration: 2,
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
scrub: 1 // 1 second of smoothing/lag
}
});
// SCRUBBING WITH DURATION
// The duration determines the distance the animation covers
// longer duration = longer distance traveled
gsap.to('.box', {
duration: 5, // Animation lasts 5 seconds of scroll
rotation: 360,
x: 1000,
scrollTrigger: {
trigger: '.section',
start: 'top top',
end: 'bottom bottom',
scrub: true
}
});
// SCRUBBING WITH TIMELINE
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.section',
start: 'top top',
end: 'bottom bottom',
scrub: 2 // 2 seconds of smoothing
}
});
tl.to('.box1', { x: 100 }, 0)
.to('.box2', { y: 100 }, 0)
.to('.box3', { rotate: 360 }, 0);
// PRACTICAL: Scroll Progress Bar
gsap.to('.progress-bar', {
width: '100%',
scrollTrigger: {
trigger: 'body',
start: 'top top',
end: 'bottom bottom',
scrub: true
}
});
Pinning freezes an element in place while the page scrolls underneath it. This is perfect for hero sections, sticky headers, or keeping elements visible while their content animates. The element stays at its position for a defined scroll distance (the end position), then unpins and scrolls normally again.
Set pin: true to pin the trigger element, or pin: '.custom-element' to pin a different element. Control the pin duration with the end position, typically using end: '+=1500' to pin for 1500px of scroll. ScrollTrigger automatically handles spacing and layout adjustments.
Scroll to see pinning in action
// BASIC PINNING - Pin the trigger element
gsap.to('.hero', {
scrollTrigger: {
trigger: '.hero',
pin: true, // Pin the .hero element
start: 'top top', // Start pinning at top
end: '+=1500', // Pin for 1500px of scroll
markers: true // Debug markers
}
});
// PIN DIFFERENT ELEMENT
gsap.to('.content', {
x: 100,
scrollTrigger: {
trigger: '.section',
pin: '.header', // Pin .header, not .section
start: 'top top',
end: 'bottom bottom'
}
});
// PIN WITH SCRUBBING (Common Pattern)
gsap.timeline({
scrollTrigger: {
trigger: '.full-screen',
pin: true,
start: 'top top',
end: '+=2000', // Pin for 2000px
scrub: 1 // Scrub animations while pinned
}
})
.to('.box', { x: 100 })
.to('.box', { y: 100 }, '<')
.to('.box', { rotate: 360 }, '<');
// PINNING WITH SPACING CONTROL
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
pin: true,
pinSpacing: false, // Don't add extra space after pin ends
start: 'top top',
end: 'bottom top'
}
});
// HORIZONTAL SCROLL WITH PIN
gsap.to('.horizontal-list', {
x: -500,
scrollTrigger: {
trigger: '.horizontal-container',
pin: true,
start: 'top top',
end: '+=1000',
scrub: true
}
});
// PRACTICAL: Sticky Hero with Animation
gsap.timeline({
scrollTrigger: {
trigger: '.hero-section',
pin: true,
start: 'top top',
end: '+=1000',
scrub: 1
}
})
.from('.hero-title', { opacity: 0, y: 100 }, 0)
.to('.hero-title', { opacity: 0, y: -100 }, '+=0.5')
.to('.hero-bg', { opacity: 0.5 }, 0);
toggleActions controls what happens when the trigger fires and unfires. Specify actions for entering/leaving the viewport in either direction. You can play, pause, resume, or reset animations based on scroll behavior. This is perfect for reversing animations when scrolling back up or toggling visibility.
The syntax is 'onEnter onLeave onEnterBack onLeaveBack'. For example, 'play pause none none' plays when entering viewport, pauses when leaving. You can also use toggleClass to add/remove CSS classes based on scroll position, enabling complex state changes with CSS.
Scroll up and down to toggle
// TOGGLE ACTIONS SYNTAX
// 'onEnter onLeave onEnterBack onLeaveBack'
// Actions: play, pause, resume, reset, restart, complete, reverse
// PLAY ON ENTER, PAUSE ON LEAVE (most common)
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
toggleActions: 'play pause none none'
// onEnter: play, onLeave: pause, onEnterBack: none, onLeaveBack: none
}
});
// PLAY FORWARD ON ENTER, REVERSE ON LEAVE
gsap.to('.box', {
duration: 1,
x: 100,
scrollTrigger: {
trigger: '.box',
toggleActions: 'play reverse none none'
// Scrolling down: play forward
// Scrolling back up: reverse animation
}
});
// PLAY AND RESTART CONTINUOUSLY
gsap.to('.box', {
duration: 1,
opacity: 0.5,
scrollTrigger: {
trigger: '.box',
toggleActions: 'restart none none none'
// Every enter triggers play from start
}
});
// NO TOGGLE (runs once)
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
toggleActions: 'play none none none'
}
});
// TOGGLE CSS CLASSES
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top 80%',
toggleClass: 'active' // Add 'active' class on enter, remove on leave
}
});
// MULTIPLE CLASSES
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top center',
toggleClass: 'in-view active visible'
}
});
// COMBINE TOGGLEACTIONS AND TOGGLECLASS
gsap.timeline({
scrollTrigger: {
trigger: '.card',
start: 'top 75%',
toggleActions: 'play none none none',
toggleClass: 'animated'
}
})
.from('.card', { opacity: 0, y: 50 })
.to('.card', { boxShadow: '0 10px 30px rgba(0,255,102,0.2)' }, 0);
// PRACTICAL: Scroll-linked class states
document.querySelectorAll('.section').forEach(section => {
gsap.to(section, {
scrollTrigger: {
trigger: section,
start: 'top center',
end: 'bottom center',
toggleClass: 'in-viewport' // CSS can target .section.in-viewport
}
});
});
Available Actions:
play - Start animation from startpause - Pause at current positionresume - Resume from pausereset - Jump to start, don't playrestart - Jump to start and playreverse - Play backwardscomplete - Jump to endnone - Do nothingCommon Patterns:
'play none none none' - Fire once'play pause none none' - Play/pause toggle'play reverse none none' - Reverse on leave'restart none none none' - Restart on each enter'play pause play pause' - Full toggle both directions
Callbacks let you execute custom code when trigger events fire. ScrollTrigger provides onEnter, onLeave, onEnterBack, onLeaveBack, onUpdate, and onRefresh callbacks. These let you synchronize animations with other code, track scroll events, or update UI elements based on trigger state.
Callbacks receive the trigger object as a parameter, letting you inspect properties like isActive, progress, and direction. This enables responsive logic where your code adapts based on scroll direction, progress through the animation, or visibility state.
Scroll to see callback events
// BASIC CALLBACKS
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
onEnter: () => console.log('Entered!'),
onLeave: () => console.log('Left!'),
onEnterBack: () => console.log('Entered again (scrolling back)'),
onLeaveBack: () => console.log('Left (scrolling back)')
}
});
// CALLBACK WITH TRIGGER OBJECT
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top center',
onEnter: (self) => {
console.log('Progress:', self.progress);
console.log('Direction:', self.direction); // 1 or -1
console.log('Is active:', self.isActive);
},
onUpdate: (self) => {
// Fires on every frame during animation
console.log('Progress:', self.progress); // 0-1
},
onRefresh: (self) => {
// Fires when ScrollTrigger recalculates (resize, etc)
console.log('Refreshed, new trigger:', self.start);
}
}
});
// PRACTICAL: Update UI based on scroll
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top center',
onEnter: () => {
document.querySelector('.indicator').classList.add('active');
},
onLeave: () => {
document.querySelector('.indicator').classList.remove('active');
}
}
});
// CONDITIONAL LOGIC IN CALLBACKS
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 75%',
onEnter: (self) => {
if (self.direction === 1) {
console.log('Scrolling down');
} else {
console.log('Scrolling up');
}
},
onUpdate: (self) => {
// Update progress bar
const progressPercent = self.progress * 100;
document.querySelector('.progress-bar').style.width = progressPercent + '%';
}
}
});
// TIMELINE WITH CALLBACKS
gsap.timeline({
scrollTrigger: {
trigger: '.section',
start: 'top center',
onEnter: () => {
console.log('Timeline started');
// Load content dynamically
loadMoreContent();
},
onLeave: () => {
console.log('Timeline left');
}
}
})
.from('.content', { opacity: 0, y: 50 })
.to('.content', { opacity: 1, y: 0 });
// MULTIPLE TRIGGERS WITH CALLBACKS
document.querySelectorAll('.card').forEach((card, i) => {
gsap.to(card, {
scrollTrigger: {
trigger: card,
start: 'top center',
onEnter: () => {
card.style.background = 'rgba(0,255,102,0.1)';
},
onLeave: () => {
card.style.background = 'transparent';
}
}
});
});
Scroll Events:
onEnter - Element enters viewportonLeave - Element leaves viewportonEnterBack - User scrolls back into triggeronLeaveBack - User scrolls back outUtility Events:
onUpdate - Every frame during animationonRefresh - When trigger recalculatesonToggle - When active/inactive state changesMarkers are visual indicators that show trigger start and end positions on the page. When debugging animations, enable markers to see exactly where your animations trigger. This is invaluable for fine-tuning animations and understanding viewport positions. Markers only appear during development—disable in production.
Enable markers with markers: true in your scrollTrigger config. You'll see three colored lines: the start position, end position, and the trigger element's boundary. ScrollTrigger also provides getAll() to inspect all active triggers and debug complex scenarios.
Markers are shown above (scroll to see them)
Red = start position | Green = end position
Scroll to see animation with markers
// ENABLE MARKERS FOR DEBUGGING
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
markers: true // Show visual markers
}
});
// CUSTOM MARKER LABELS
gsap.to('.box', {
x: 100,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
markers: {
startColor: 'white',
endColor: 'fuchsia',
fontSize: '16px'
}
}
});
// INSPECT ALL ACTIVE TRIGGERS
const allTriggers = ScrollTrigger.getAll();
console.log('Total triggers:', allTriggers.length);
allTriggers.forEach(trigger => {
console.log({
trigger: trigger.trigger,
start: trigger.start,
end: trigger.end,
progress: trigger.progress,
isActive: trigger.isActive,
direction: trigger.direction
});
});
// GET SPECIFIC TRIGGER
const trigger = ScrollTrigger.getById('myTrigger');
// DEBUGGING: Log all trigger data
gsap.to('.box', {
x: 100,
id: 'myTrigger', // Give it an ID
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
onEnter: (self) => {
console.log('Trigger data:', {
progress: self.progress,
direction: self.direction,
isActive: self.isActive,
getVelocity: self.getVelocity()
});
}
}
});
// REFRESH TRIGGERS MANUALLY
ScrollTrigger.refresh();
// DISABLE ALL TRIGGERS TEMPORARILY
ScrollTrigger.disable();
// RE-ENABLE TRIGGERS
ScrollTrigger.enable();
// KILL SPECIFIC TRIGGER
const tween = gsap.to('.box', { x: 100, scrollTrigger: { trigger: '.box' } });
tween.scrollTrigger.kill();
// KILL ALL TRIGGERS
ScrollTrigger.getAll().forEach(trigger => trigger.kill());
// PRACTICAL: Development-only markers
const isDev = true; // Set based on environment
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top center',
markers: isDev, // Only show in development
onEnter: isDev ? () => console.log('Debug: Entered') : undefined
}
});
Advanced ScrollTrigger patterns include batch processing for performance, responsive configurations with matchMedia, horizontal scrolling, snap positioning, and complex nested timelines. These techniques enable professional production-quality animations that handle edge cases, respond to screen size changes, and maintain performance with many elements.
Master batch(), matchMedia(), and snap() to build sophisticated, responsive scroll experiences that work across all devices. These patterns are essential for production applications where performance and responsiveness are critical requirements.
Scroll to see batch animation
// BATCH: Optimize many similar triggers
// More performant than creating individual triggers
ScrollTrigger.batch('.card', {
onEnter: (elements) => {
gsap.to(elements, {
opacity: 1,
y: 0,
duration: 0.8,
stagger: 0.15
});
},
onLeave: (elements) => {
gsap.to(elements, { opacity: 0.5 });
},
start: 'top 90%',
interval: 200 // Wait 200ms between batches
});
// MATCH MEDIA: Responsive ScrollTrigger
gsap.matchMedia().add(
{
isDesktop: '(min-width: 1024px)',
isMobile: '(max-width: 768px)'
},
(context) => {
let { isDesktop, isMobile } = context.conditions;
if (isDesktop) {
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top center',
pin: true,
end: '+=1000'
}
});
}
if (isMobile) {
gsap.to('.section', {
scrollTrigger: {
trigger: '.section',
start: 'top 75%'
}
});
}
return () => {
// Optional: cleanup code
};
}
);
// SNAP: Snap to specific timeline positions
gsap.timeline({
scrollTrigger: {
trigger: '.section',
pin: true,
start: 'top top',
end: '+=2000',
scrub: 1,
snap: {
snapTo: 0.5, // Snap to 50% increments
duration: 0.5,
delay: 0
}
}
})
.to('.box1', { x: 100 })
.to('.box2', { y: 100 }, 0);
// SNAP WITH CUSTOM ARRAY
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top top',
end: 'bottom bottom',
scrub: 1,
snap: [0, 0.25, 0.5, 0.75, 1] // Snap to specific points
}
});
// HORIZONTAL SCROLL WITH PIN
gsap.to('.horizontal-list', {
x: -500,
duration: 1,
scrollTrigger: {
trigger: '.horizontal-container',
pin: true,
start: 'top top',
end: '+=1000',
scrub: 1,
onUpdate: (self) => {
// Custom logic during horizontal scroll
}
}
});
// PARALLAX LAYERS
document.querySelectorAll('.parallax-layer').forEach((layer, i) => {
gsap.to(layer, {
y: (i + 1) * 100, // Each layer moves at different speed
scrollTrigger: {
trigger: '.parallax-section',
start: 'top center',
end: 'bottom center',
scrub: true,
markers: false
}
});
});
// COMPLEX NESTED TIMELINE WITH SCROLLTRIGGER
gsap.timeline({
scrollTrigger: {
trigger: '.complex-section',
pin: true,
start: 'top top',
end: '+=2000',
scrub: 1,
snap: 0.25,
onUpdate: (self) => {
// Sync other UI elements
updateProgressBar(self.progress);
}
}
})
.from('.intro', { opacity: 0, y: 50 }, 0)
.to('.intro', { opacity: 0, y: -50 }, 0.25)
.from('.content', { opacity: 0 }, 0.25)
.to('.content', { y: 100 }, 0.5)
.to('.bg', { opacity: 0 }, 0);
// PERFORMANCE: Kill unused triggers
function cleanupScrollTriggers(selector) {
ScrollTrigger.getAll()
.filter(trigger => trigger.trigger.matches(selector))
.forEach(trigger => trigger.kill());
}
// Dynamic content: refresh triggers
async function loadMoreContent() {
const html = await fetch('/more-content').then(r => r.text());
document.getElementById('container').innerHTML += html;
ScrollTrigger.refresh(); // Recalculate all trigger positions
}
Combining ScrollTrigger with GSAP timelines lets you create complex, choreographed scroll animations. Rather than individual scroll-triggered tweens, timelines orchestrate multiple animations in sequence based on scroll position. This is perfect for scroll stories where multiple elements animate in a coordinated sequence.
// Timeline with ScrollTrigger
let tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
start: "top top",
end: "bottom top",
scrub: 1,
pin: true
}
});
tl.to(".box1", { x: 500 })
.to(".box2", { y: 300 })
.to(".box3", { rotation: 360 });
// Multiple ScrollTriggers with Timeline
let tl = gsap.timeline();
tl.from(".heading", { opacity: 0, y: 50 })
.from(".subheading", { opacity: 0, y: 30 }, "-=0.3")
.from(".content", { opacity: 0 }, "-=0.2");
ScrollTrigger.create({
trigger: ".section",
start: "top 80%",
animation: tl
});
gsap.timeline({ scrollTrigger: {...} }).to(), .from(), .fromTo()'<' for parallel animationsUsing ScrollTrigger in React requires proper cleanup and lifecycle management. Always register the plugin, use refs to target elements, and kill triggers on component unmount to prevent memory leaks.
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function ScrollComponent() {
const boxRef = useRef(null);
useEffect(() => {
const element = boxRef.current;
gsap.to(element, {
scrollTrigger: {
trigger: element,
start: "top center",
end: "bottom top",
scrub: true,
markers: true
},
x: 500
});
// Cleanup on unmount
return () => {
ScrollTrigger.getAll().forEach(st => st.kill());
};
}, []);
return <div ref={boxRef} className="box">Scroll Me</div>;
}
import { useRef } from 'react';
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function ScrollComponent() {
const container = useRef();
useGSAP(() => {
gsap.to(".box", {
scrollTrigger: {
trigger: ".box",
scrub: true
},
x: 500
});
}, { scope: container }); // Auto cleanup!
return <div ref={container}>...</div>;
}
Since Webflow's native GSAP integration in 2025, using ScrollTrigger is easier than ever. Add custom code in Embed elements or Page Settings and leverage Webflow's class names for targeting.
<script>
// Wait for page load
document.addEventListener('DOMContentLoaded', () => {
// Register plugin (already loaded in Webflow)
gsap.registerPlugin(ScrollTrigger);
// Your animations
gsap.to(".hero-image", {
scrollTrigger: {
trigger: ".hero-section",
start: "top top",
end: "bottom top",
scrub: 1
},
y: 200,
scale: 1.2
});
});
</script>
// Target Webflow combo classes
gsap.to(".card.is-featured", {
scrollTrigger: ".card.is-featured",
scale: 1.1
});
// Animate collection items
gsap.from(".collection-item", {
scrollTrigger: {
trigger: ".collection-list",
start: "top 80%"
},
y: 50,
opacity: 0,
stagger: 0.2
});
Here are production-ready examples you can copy and customize for your projects. Each example demonstrates a common scroll animation pattern.
gsap.from(".fade-in", {
scrollTrigger: {
trigger: ".fade-in",
start: "top 80%",
toggleActions: "play none none reverse"
},
opacity: 0,
y: 50,
duration: 1
});
gsap.to(".parallax-bg", {
scrollTrigger: {
trigger: ".hero-section",
start: "top top",
end: "bottom top",
scrub: true
},
y: 300,
ease: "none"
});
let sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1),
end: () => "+=" + document.querySelector(".container").offsetWidth
}
});
These three examples demonstrate fade-in, parallax, and horizontal scroll—the most common ScrollTrigger patterns. Explore the complete guide for 20+ additional examples covering all use cases.
Use batch() for many elements, avoid onUpdate for heavy calculations, kill triggers when elements are removed. Performance is critical for scroll animations.
Test extensively on mobile. Use matchMedia() for different animations at different breakpoints. Pin durations, timing, and scroll distances vary on mobile.
Call ScrollTrigger.refresh() after window resize, content changes, or images load. Triggers won't recalculate automatically for some changes.
Enable markers, log in callbacks, test viewport positions. Use ScrollTrigger.getAll() to inspect all active triggers and verify positions.
Check prefers-reduced-motion and disable ScrollTrigger animations for users who prefer no motion. Animations should enhance, not hinder usability.
Plan start/end positions before coding. Sketch viewport positions and trigger moments. Proper planning prevents timing bugs and improves animation quality.
gsap.to(element, { x: 100, scrollTrigger: { trigger: element, start: 'top 80%' } }). When the top of the element hits 80% of the viewport height, the animation plays. Adjust start/end positions to control when animations trigger. Use start: 'top center' for center viewport, start: 'top top' for top of viewport.
scrub: true for instant scrubbing, or scrub: 1 for 1 second of smoothing. This creates interactive effects where users control animations by scrolling—perfect for scroll stories, progress indicators, and parallax layers. The duration of your animation determines how far it travels during the scroll range.
pin: true in scrollTrigger config and define an end position. The element stays visible at its position while the page scrolls underneath. Use pin: '.container' to pin a specific element. Control the pin duration with end: '+=1500' (1500px of scroll) or end: 'bottom bottom'. ScrollTrigger automatically handles spacing and layout adjustments.
'onEnter onLeave onEnterBack onLeaveBack'. Example: 'play pause none none' plays when entering viewport, pauses when leaving. Use them to reverse animations or create toggle effects based on scroll direction. Actions include: play, pause, resume, reset, restart, complete, reverse, and none.
markers: true in scrollTrigger config to see visual start/end markers on the page. Use the ScrollTrigger.getAll() method to inspect all active triggers in the console. Log in callbacks (onEnter, onLeave, onUpdate) to trace what's happening. The browser DevTools console is your friend—log trigger progress, direction, and active state to understand behavior.
gsap.utils.toArray() and forEach() to loop through elements. Or use ScrollTrigger.batch() for better performance with many elements—it groups them and staggers initialization. Example: ScrollTrigger.batch('.card', { onEnter: (elements) => { gsap.to(elements, {...}) } }). Batch is more efficient than individual triggers for large numbers of elements.
gsap.matchMedia() to apply different configs at different breakpoints. Or use ScrollTrigger.refresh() after window resize to recalculate trigger positions. Test thoroughly on mobile devices since trigger positions change with viewport size. Many animations work great on desktop but need adjustment for mobile—use matchMedia to provide device-specific experiences.
You now have all the knowledge to create sophisticated, scroll-driven animations. From simple trigger-based animations to complex pinned sections with scrubbing and parallax, you can build anything. The key is practice—start small, experiment with timing, and scale up to complex patterns.