GSAP Scroll — ScrollTrigger Patterns
Flow: gsap-setup → gsap-animate → gsap-scroll → gsap-optimise → gsap-test
Cross-reference gsap-animate for context/cleanup, gsap-optimise for batch/scrub tuning.
Companion: For ScrollTrigger API reference, invoke gsap-scrolltrigger. This skill covers scroll animation recipes only. Requires: greensock/gsap-skills
1. Basic Scroll Reveal
ctx = gsap.context(() => {
const els = gsap.utils.toArray('.reveal', sectionRef.value)
gsap.set(els, { y: 28, autoAlpha: 0 })
els.forEach((el) => {
gsap.to(el, {
y: 0, autoAlpha: 1, duration: 0.8, ease: 'power2.out', force3D: true,
scrollTrigger: {
trigger: el, start: 'top 88%',
toggleActions: 'play none none reverse',
invalidateOnRefresh: true,
},
})
})
}, sectionRef.value)
autoAlpha not opacity — sets visibility: hidden at 0
toggleActions: 'play none none reverse' — plays on enter, reverses on leave-back
2. ScrollTrigger.batch() — Masonry/Grid Reveal
ctx = gsap.context(() => {
const cards = gsap.utils.toArray('.masonry-card')
gsap.set(cards, { y: 60, autoAlpha: 0, rotation: () => gsap.utils.random(-3, 3) })
const ENTER = { y: 0, autoAlpha: 1, rotation: 0, stagger: 0.08, duration: 0.8,
ease: 'power3.out', force3D: true, overwrite: 'auto' }
const EXIT_UP = { y: -20, autoAlpha: 0, duration: 0.4, ease: 'power2.in', overwrite: 'auto' }
const EXIT_DOWN = { y: 60, autoAlpha: 0, duration: 0.4, ease: 'power2.in', overwrite: 'auto' }
ScrollTrigger.batch(cards, {
onEnter: (batch) => gsap.to(batch, ENTER),
onLeave: (batch) => gsap.to(batch, EXIT_UP),
onEnterBack: (batch) => gsap.to(batch, ENTER),
onLeaveBack: (batch) => gsap.to(batch, EXIT_DOWN),
start: 'top 85%', end: 'bottom 15%',
})
}, sectionRef.value)
overwrite: 'auto' prevents conflicting tweens on fast scroll
- Functional
rotation re-evaluates per element for organic randomness
3. Parallax Layers
<div class="parallax-layer" data-speed="0.2"><!-- slow --></div>
<div class="parallax-layer" data-speed="0.6"><!-- fast --></div>
ctx = gsap.context(() => {
gsap.utils.toArray('.parallax-layer', sectionRef.value).forEach((layer) => {
const speed = parseFloat(layer.dataset.speed) || 0.5
gsap.to(layer, {
yPercent: -50 * speed, ease: 'none', force3D: true,
scrollTrigger: {
trigger: sectionRef.value, start: 'top bottom', end: 'bottom top', scrub: 0.5, invalidateOnRefresh: true,
},
})
})
}, sectionRef.value)
yPercent not y — percentage-based, scales with element size
scrub: 0.5 — smoother than scrub: true, 0.5s catch-up
4. Refresh Patterns
await nextTick(); ScrollTrigger.refresh() // v-if toggles
img.addEventListener('load', () => ScrollTrigger.refresh()) // lazy images
document.fonts.ready.then(() => ScrollTrigger.refresh()) // font swap
ScrollTrigger.config({ ignoreMobileResize: true }) // address bar
Advanced Patterns
See references/scroll-patterns.md for:
- Stacking Cards — CSS sticky + scrubbed two-phase timeline with will-change lifecycle
- Scrubbed Timeline — progress bar, bouncing dots, alternating card reveals
- Elastic Type Assembly — pin + scrub three-phase scatter/assemble/scatter
- Service Section Switching — ScrollTrigger.create() with activate/deactivate callbacks
- Velocity Skew — skewY transforms driven by ScrollTrigger.getVelocity() + gsap.quickSetter
- Infinite Looped Panels — pinSpacing: false stacking with seamless scroll boundary looping
- Directionally Aware Header — show/hide fixed header based on scroll direction
- Pinned Panels with Overscroll — slide-based pinning with fake-scroll for tall panels + scale/fade exit
- Image Mask On Scroll — before/after image reveal using counter-translating containers
- Lateral Pin Indicator — pinned section with side nav indicator + crossfading slide content
- Horizontal Scrolling Gallery — horizontal scroll via pin + scrub with xPercent translation
Rules
- Wrap all ScrollTrigger tweens in
gsap.context() scoped to a ref
- Use
autoAlpha not opacity for reveals
- Include
invalidateOnRefresh: true when animating positional values
- Use
overwrite: 'auto' or true when elements can be re-triggered
- Manage
will-change lifecycle in ST callbacks (set on enter, release on leave)
- Call
ScrollTrigger.refresh() after DOM changes that affect document height
- Prefer
scrub: 0.5 over scrub: true for smoother motion
1---2name: gsap-scroll3description: Production recipes for scroll-based GSAP animations. Companion to official gsap-scrolltrigger skill (API reference). Triggers: ScrollTrigger, scroll animation, scroll reveal, parallax, sticky cards, stacking cards, scrub, pin section, batch reveal, masonry reveal, scroll progress, timeline scrub, elastic type, section switching, ScrollTrigger.batch, ScrollTrigger.create, scroll refresh, scroll cleanup, GSAP scroll, Vue scroll animation, Nuxt scroll animation. Non-triggers: Not for mouse-driven interactions (use gsap-interact), text effects (use gsap-text), SVG path drawing (use gsap-svg), or visual effects (use gsap-vfx). Outcome: Produces scroll-triggered animations — reveals, batch staggers, parallax layers, pinned sections, and scrubbed timelines.4---56# GSAP Scroll — ScrollTrigger Patterns78> **Flow**: gsap-setup → gsap-animate → **gsap-scroll** → gsap-optimise → gsap-test9> Cross-reference gsap-animate for context/cleanup, gsap-optimise for batch/scrub tuning.1011> **Companion**: For ScrollTrigger API reference, invoke **gsap-scrolltrigger**. This skill covers scroll animation recipes only. Requires: `greensock/gsap-skills`1213---1415## 1. Basic Scroll Reveal1617```js18ctx = gsap.context(() => {19 const els = gsap.utils.toArray('.reveal', sectionRef.value)20 gsap.set(els, { y: 28, autoAlpha: 0 })2122 els.forEach((el) => {23 gsap.to(el, {24 y: 0, autoAlpha: 1, duration: 0.8, ease: 'power2.out', force3D: true,25 scrollTrigger: {26 trigger: el, start: 'top 88%',27 toggleActions: 'play none none reverse',28 invalidateOnRefresh: true,29 },30 })31 })32}, sectionRef.value)33```3435- `autoAlpha` not `opacity` — sets `visibility: hidden` at 036- `toggleActions: 'play none none reverse'` — plays on enter, reverses on leave-back3738---3940## 2. ScrollTrigger.batch() — Masonry/Grid Reveal4142```js43ctx = gsap.context(() => {44 const cards = gsap.utils.toArray('.masonry-card')45 gsap.set(cards, { y: 60, autoAlpha: 0, rotation: () => gsap.utils.random(-3, 3) })4647 const ENTER = { y: 0, autoAlpha: 1, rotation: 0, stagger: 0.08, duration: 0.8,48 ease: 'power3.out', force3D: true, overwrite: 'auto' }49 const EXIT_UP = { y: -20, autoAlpha: 0, duration: 0.4, ease: 'power2.in', overwrite: 'auto' }50 const EXIT_DOWN = { y: 60, autoAlpha: 0, duration: 0.4, ease: 'power2.in', overwrite: 'auto' }5152 ScrollTrigger.batch(cards, {53 onEnter: (batch) => gsap.to(batch, ENTER),54 onLeave: (batch) => gsap.to(batch, EXIT_UP),55 onEnterBack: (batch) => gsap.to(batch, ENTER),56 onLeaveBack: (batch) => gsap.to(batch, EXIT_DOWN),57 start: 'top 85%', end: 'bottom 15%',58 })59}, sectionRef.value)60```6162- `overwrite: 'auto'` prevents conflicting tweens on fast scroll63- Functional `rotation` re-evaluates per element for organic randomness6465---6667## 3. Parallax Layers6869```html70<div class="parallax-layer" data-speed="0.2"><!-- slow --></div>71<div class="parallax-layer" data-speed="0.6"><!-- fast --></div>72```7374```js75ctx = gsap.context(() => {76 gsap.utils.toArray('.parallax-layer', sectionRef.value).forEach((layer) => {77 const speed = parseFloat(layer.dataset.speed) || 0.578 gsap.to(layer, {79 yPercent: -50 * speed, ease: 'none', force3D: true,80 scrollTrigger: {81 trigger: sectionRef.value, start: 'top bottom', end: 'bottom top', scrub: 0.5, invalidateOnRefresh: true,82 },83 })84 })85}, sectionRef.value)86```8788- `yPercent` not `y` — percentage-based, scales with element size89- `scrub: 0.5` — smoother than `scrub: true`, 0.5s catch-up9091---9293## 4. Refresh Patterns9495```js96await nextTick(); ScrollTrigger.refresh() // v-if toggles97img.addEventListener('load', () => ScrollTrigger.refresh()) // lazy images98document.fonts.ready.then(() => ScrollTrigger.refresh()) // font swap99ScrollTrigger.config({ ignoreMobileResize: true }) // address bar100```101102---103104## Advanced Patterns105106See `references/scroll-patterns.md` for:107- **Stacking Cards** — CSS sticky + scrubbed two-phase timeline with will-change lifecycle108- **Scrubbed Timeline** — progress bar, bouncing dots, alternating card reveals109- **Elastic Type Assembly** — pin + scrub three-phase scatter/assemble/scatter110- **Service Section Switching** — ScrollTrigger.create() with activate/deactivate callbacks111- **Velocity Skew** — skewY transforms driven by ScrollTrigger.getVelocity() + gsap.quickSetter112- **Infinite Looped Panels** — pinSpacing: false stacking with seamless scroll boundary looping113- **Directionally Aware Header** — show/hide fixed header based on scroll direction114- **Pinned Panels with Overscroll** — slide-based pinning with fake-scroll for tall panels + scale/fade exit115- **Image Mask On Scroll** — before/after image reveal using counter-translating containers116- **Lateral Pin Indicator** — pinned section with side nav indicator + crossfading slide content117- **Horizontal Scrolling Gallery** — horizontal scroll via pin + scrub with xPercent translation118119---120121## Rules1221231. Wrap all ScrollTrigger tweens in `gsap.context()` scoped to a ref1242. Use `autoAlpha` not `opacity` for reveals1253. Include `invalidateOnRefresh: true` when animating positional values1264. Use `overwrite: 'auto'` or `true` when elements can be re-triggered1275. Manage `will-change` lifecycle in ST callbacks (set on enter, release on leave)1286. Call `ScrollTrigger.refresh()` after DOM changes that affect document height1297. Prefer `scrub: 0.5` over `scrub: true` for smoother motion