gsap-scrolltrigger
Installation
SKILL.md
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%', // When top of box hits 80% of viewport
end: 'top 20%', // When top of box hits 20% of viewport
scrub: true // Link animation to scroll
}
})
Scrub with Smoothing
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scrub: 1 // 1 second smoothing
}
})
Pinning
Basic Pin
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500', // Pin for 500px of scroll
pin: true, // Pin the trigger element
scrub: true
}
})
Pin with Spacing Control
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: '+=500',
pin: true,
pinSpacing: false // Don't add extra spacing
}
})
Pin Different Element
gsap.to('.content', {
x: 500,
scrollTrigger: {
trigger: '.trigger-element',
start: 'top center',
end: '+=500',
pin: '.pin-element', // Pin different element
scrub: true
}
})
Toggle Actions
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: 'bottom center',
toggleActions: 'play none none reverse'
// Format: onEnter, onLeave, onEnterBack, onLeaveBack
// Options: play, pause, resume, reset, restart, complete, reverse, none
}
})
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',
// Scroll event callbacks
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),
// Scrub complete
onScrubComplete: (self) => console.log('Scrub complete'),
// Refresh events
onRefresh: (self) => console.log('Refreshed'),
onRefreshInit: (self) => console.log('Refresh init')
}
})
Accessing ScrollTrigger Data
ScrollTrigger.create({
trigger: '.box',
start: 'top center',
onUpdate: (self) => {
console.log('Progress:', self.progress) // 0-1
console.log('Direction:', self.direction) // 1 or -1
console.log('Velocity:', self.getVelocity()) // Scroll velocity
console.log('Active:', self.isActive) // Boolean
}
})
Position Syntax
Basic Positions
scrollTrigger: {
trigger: '.box',
// Viewport positions
start: 'top center', // Element top hits viewport center
end: 'bottom top', // Element bottom hits viewport top
// Pixel values
start: 'top 100px', // Element top hits 100px from viewport top
end: 'top 500px',
// Percentage
start: 'top 80%', // Element top hits 80% viewport
end: 'top 20%',
// Relative values
start: 'top center',
end: '+=500', // 500px after start
// Function
start: (self) => {
return self.trigger.offsetHeight * 0.8
}
}
Position Keywords
top/bottom/center/left/right: Element edgetop center- Element top to viewport centercenter center- Element center to viewport centerbottom 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 0.1 increments (10%)
}
})
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 to specific values
}
})
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 // Show visual markers
}
})
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
// Parent timeline
const parentTl = gsap.timeline({
scrollTrigger: {
trigger: '.parent',
start: 'top top',
end: '+=1000',
pin: true,
scrub: true
}
})
// Nested ScrollTrigger
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
// Add content dynamically
document.querySelector('.container').innerHTML = newContent
// Refresh ScrollTrigger
ScrollTrigger.refresh()
Scroller Customization
Custom Scroller
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top 80%',
scroller: '.custom-scroller', // Custom scroll container
scrub: true
}
})
Multiple Scrollers
// Container 1
gsap.to('.box1', {
x: 500,
scrollTrigger: {
trigger: '.box1',
scroller: '.scroller1',
start: 'top center',
scrub: true
}
})
// Container 2
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')
})
// Manually trigger
st.scroll(st.start) // Jump to start position
st.scroll(st.end) // Jump to end position
// Get position
console.log(st.start)
console.log(st.end)
console.log(st.progress)
// Update
st.refresh()
st.update()
// Enable/disable
st.enable()
st.disable()
// Kill
st.kill()
// Get velocity
st.getVelocity()
// Check if active
console.log(st.isActive)
Static Methods
// Get all ScrollTriggers
const triggers = ScrollTrigger.getAll()
// Refresh all
ScrollTrigger.refresh()
// Scroll to position
ScrollTrigger.scroll(position)
// Create standalone instance
const st = ScrollTrigger.create({
trigger: '.box',
start: 'top center',
onEnter: () => console.log('Entered')
})
// Match media with ScrollTrigger
gsap.matchMedia().add('(min-width: 768px)', () => {
ScrollTrigger.refresh()
})
Performance Tips
Use Scrub Sparingly
// ❌ Heavy scrub on many elements
gsap.utils.toArray('.item').forEach(item => {
gsap.to(item, {
x: 100,
scrollTrigger: {
trigger: item,
scrub: true
}
})
})
// ✅ Batch or use toggle actions
gsap.to('.item', {
x: 100,
scrollTrigger: {
trigger: '.container',
start: 'top center',
toggleActions: 'play none none reverse'
}
})
Refresh Only When Needed
// ❌ Refresh on every scroll
window.addEventListener('scroll', () => {
ScrollTrigger.refresh()
})
// ✅ Refresh after DOM changes
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 // Pin 1 scroll tick early
}
})
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
// ❌ DOM changes but ScrollTrigger uses old positions
dynamicContent.innerHTML = newContent
// ✅ Refresh after changes
dynamicContent.innerHTML = newContent
ScrollTrigger.refresh()
2. Wrong Scroller
// ❌ Using default scroller when using custom
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scrub: true
// Missing scroller: '.custom-scroller'
}
})
// ✅ Specify custom scroller
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
scroller: '.custom-scroller',
scrub: true
}
})
3. Pin Conflicts
// ❌ Multiple pins causing issues
gsap.to('.box1', { x: 500, scrollTrigger: { trigger: '.box1', pin: true } })
gsap.to('.box2', { x: 500, scrollTrigger: { trigger: '.box2', pin: true } })
// ✅ Use nested timeline or check overlap
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' |
Weekly Installs
1
Repository
microck/gsap-skillsFirst Seen
6 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1