GSAP ScrollTrigger
Scroll-driven animations and interactions.
Quick Start npm 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 #00F5FF, 0 0 100px #00F5FF', 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();
Reference See gsap-fundamentals for animation basics See gsap-sequencing for timeline composition See gsap-react for React integration with ScrollTrigger