| name | gsap-scrolltrigger |
| description | Use when implementing scroll-based animations, scroll-driven effects, pinning elements, scrubbing animations, scroll callbacks, or when using ScrollTrigger plugin. |
GSAP ScrollTrigger
ScrollTrigger links animations to the scroll position of a page or container. Enable scrubbing, pinning, snapping, and callbacks for scroll-driven experiences.
Installation
npm install gsap
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
Basic Usage
Simple Scroll Animation
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
end: 'top 20%',
scrub: true
}
})
Scrub with Smoothing
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scrub: 1
}
})
Pinning
Basic Pin
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
pin: true,
scrub: true
}
})
Pin with Spacing Control
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
pin: true,
pinSpacing: false
}
})
Pin Different Element
gsap.to('.content', {
x: 500,
scrollTrigger: {
trigger: '.trigger-element',
start: 'top center',
end: '+=500',
pin: '.pin-element',
scrub: true
}
})
Toggle Actions
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: 'bottom center',
toggleActions: 'play none none reverse'
}
})
Toggle action examples:
'play none none reverse' - Play on scroll down, reverse on scroll up
'play pause resume reset' - Play down, pause, resume on scroll up, reset
'restart none none none' - Always restart when entering
'none none none reverse' - Only animate when scrolling up
Callbacks
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
onEnter: (self) => console.log('Entered', self.progress),
onLeave: (self) => console.log('Left', self.progress),
onEnterBack: (self) => console.log('Re-entered', self.progress),
onLeaveBack: (self) => console.log('Re-left', self.progress),
onUpdate: (self) => console.log('Update', self.progress),
onToggle: (self) => console.log('Toggled', self.isActive),
: .(),
: .(),
: .()
}
})
Accessing ScrollTrigger Data
ScrollTrigger.create({
trigger: '.box',
start: 'top center',
onUpdate: (self) => {
console.log('Progress:', self.progress)
console.log('Direction:', self.direction)
console.log('Velocity:', self.getVelocity())
console.log('Active:', self.isActive)
}
})
Position Syntax
Basic Positions
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: 'bottom top',
start: 'top 100px',
end: 'top 500px',
start: 'top 80%',
end: 'top 20%',
start: 'top center',
end: '+=500',
start: (self) => {
return self.trigger.offsetHeight * 0.8
}
}
Position Keywords
top / bottom / center / left / right: Element edge
top center - Element top to viewport center
center center - Element center to viewport center
bottom 80% - Element bottom to 80% from viewport top
Timeline Integration
Timeline with ScrollTrigger
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.section',
start: 'top center',
end: 'bottom top',
scrub: true,
pin: true
}
})
tl.to('.box1', { x: 100, duration: 1 })
.to('.box2', { x: 100, duration: 1 })
.to('.box3', { x: 100, duration: 1 })
Multiple ScrollTriggers on Timeline
const tl = gsap.timeline()
tl.to('.box1', {
x: 100,
scrollTrigger: {
trigger: '.box1',
start: 'top center',
scrub: true
}
})
tl.to('.box2', {
y: 100,
scrollTrigger: {
trigger: '.box2',
start: 'top center',
scrub: true
}
})
Snapping
Basic Snap
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
scrub: 1,
snap: 0.1
}
})
Snap to Values
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
scrub: 1,
snap: [0, 0.25, 0.5, 0.75, 1]
}
})
Snap with Direction
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
scrub: 1,
snap: {
snapTo: [0.25, 0.5, 0.75, 1],
duration: { min: 0.2, max: 0.5 },
ease: 'power1.inOut',
inertia: true
}
}
})
Markers
Enable Markers
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
scrub: true,
markers: true
}
})
Custom Markers
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
scrub: true,
markers: {
startColor: 'green',
endColor: 'red',
fontSize: '18px',
indent: 20,
name: 'My Trigger'
}
}
})
Horizontal Scroll
Basic Horizontal
gsap.to('.container', {
xPercent: -100 * (sections.length - 1),
scrollTrigger: {
trigger: '.wrapper',
start: 'top top',
end: '+=3000',
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1)
}
})
Horizontal with Sections
const sections = gsap.utils.toArray('.section')
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
scrollTrigger: {
trigger: '.container',
start: 'top top',
end: '+=3000',
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1)
}
})
Nested ScrollTriggers
const parentTl = gsap.timeline({
scrollTrigger: {
trigger: '.parent',
start: 'top top',
end: '+=1000',
pin: true,
scrub: true
}
})
parentTl.to('.child', {
rotation: 360,
scrollTrigger: {
trigger: '.child',
start: 'top center',
end: 'bottom center',
scrub: true
}
})
Batch Operations
Batch with ScrollTrigger
ScrollTrigger.batch('.box', {
onEnter: batch => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.1 }),
onLeave: batch => gsap.to(batch, { opacity: 0, y: 50 }),
onEnterBack: batch => gsap.to(batch, { opacity: 1, y: 0 }),
onLeaveBack: batch => gsap.to(batch, { opacity: 0, y: 50 })
})
Batch with Scrub
ScrollTrigger.batch('.box', {
start: 'top bottom-=100',
onEnter: batch => gsap.to(batch, {
scale: 1,
opacity: 1,
stagger: 0.1,
overwrite: 'auto',
scrollTrigger: {
trigger: batch,
start: 'top bottom-=100',
end: 'bottom top',
scrub: true
}
})
})
Dynamic Triggers
Dynamic Content
function createDynamicTrigger() {
const box = document.createElement('div')
box.className = 'box'
document.body.appendChild(box)
gsap.to(box, {
x: 500,
scrollTrigger: {
trigger: box,
start: 'top center',
end: '+=500',
scrub: true
}
})
return box
}
Refresh After DOM Changes
document.querySelector('.container').innerHTML = newContent
ScrollTrigger.refresh()
Scroller Customization
Custom Scroller
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
scroller: '.custom-scroller',
scrub: true
}
})
Multiple Scrollers
gsap.to('.box1', {
x: 500,
scrollTrigger: {
trigger: '.box1',
scroller: '.scroller1',
start: 'top center',
scrub: true
}
})
gsap.to('.box2', {
x: 500,
scrollTrigger: {
trigger: '.box2',
scroller: '.scroller2',
start: 'top center',
scrub: true
}
})
Instance Methods
const st = ScrollTrigger.create({
trigger: '.box',
start: 'top center',
onEnter: () => console.log('Entered')
})
st.scroll(st.start)
st.scroll(st.end)
console.log(st.start)
console.log(st.end)
console.log(st.progress)
st.refresh()
st.update()
st.enable()
st.disable()
st.kill()
st.getVelocity()
console.log(st.isActive)
Static Methods
const triggers = ScrollTrigger.getAll()
ScrollTrigger.refresh()
ScrollTrigger.scroll(position)
const st = ScrollTrigger.create({
trigger: '.box',
start: 'top center',
onEnter: () => console.log('Entered')
})
gsap.matchMedia().add('(min-width: 768px)', () => {
ScrollTrigger.refresh()
})
Performance Tips
Use Scrub Sparingly
gsap.utils.toArray('.item').forEach(item => {
gsap.to(item, {
x: 100,
scrollTrigger: {
trigger: item,
scrub: true
}
})
})
gsap.to('.item', {
x: 100,
scrollTrigger: {
trigger: '.container',
start: 'top center',
toggleActions: 'play none none reverse'
}
})
Refresh Only When Needed
window.addEventListener('scroll', () => {
ScrollTrigger.refresh()
})
function addContent() {
document.querySelector('.container').innerHTML = newContent
ScrollTrigger.refresh()
}
Use Anticipate Pin
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
pin: true,
anticipatePin: 1
}
})
Common Patterns
Parallax Effect
gsap.to('.parallax-bg', {
yPercent: 50,
scrollTrigger: {
trigger: '.section',
start: 'top bottom',
end: 'bottom top',
scrub: true
}
})
Reveal on Scroll
gsap.from('.reveal', {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: '.reveal',
start: 'top 80%',
toggleActions: 'play none none reverse'
},
stagger: 0.2
})
Progress Indicator
ScrollTrigger.create({
trigger: '.section',
start: 'top top',
end: 'bottom bottom',
onUpdate: (self) => {
gsap.to('.progress-bar', {
scaleX: self.progress
})
}
})
Common Mistakes
1. Forgetting Refresh
dynamicContent.innerHTML = newContent
dynamicContent.innerHTML = newContent
ScrollTrigger.refresh()
2. Wrong Scroller
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scrub: true
}
})
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scroller: '.custom-scroller',
scrub: true
}
})
3. Pin Conflicts
gsap.to('.box1', { x: 500, scrollTrigger: { trigger: '.box1', pin: true } })
gsap.to('.box2', { x: 500, scrollTrigger: { trigger: '.box2', pin: true } })
const tl = gsap.timeline({ scrollTrigger: { trigger: '.container', pin: true } })
tl.to('.box1', { x: 500 })
.to('.box2', { x: 500 })
Best Practices
- Use scrub wisely - Good for visual control, bad for performance with many elements
- Pin only when necessary - Pinning adds complexity
- Leverage toggle actions - More performant than scrub for simple effects
- Refresh after DOM changes - Critical for dynamic content
- Use markers for development - Remove in production
- Combine with timelines - Easier to control complex sequences
- Consider performance - Batch similar animations, avoid over-scrubbing
Quick Reference
| Feature | Method |
|---|
| Register plugin | gsap.registerPlugin(ScrollTrigger) |
| Basic scroll animation | gsap.to(target, { scrollTrigger: { trigger, start, scrub } }) |
| Pin element | pin: true |
| Scrub with smoothing | scrub: 1 |
| Toggle actions | toggleActions: 'play none none reverse' |
| Callbacks | onEnter, onLeave, onUpdate |
| Refresh | ScrollTrigger.refresh() |
| Get all triggers | ScrollTrigger.getAll() |
| Markers | markers: true |
| Snap | snap: 0.1 |
| Custom scroller | scroller: '.container' |