microck/gsap-skills · Archived

gsap-scrolltrigger

Use when implementing scroll-based animations, scroll-driven effects, pinning elements, scrubbing animations, scroll callbacks, or when using ScrollTrigger plugin.

First seen Apr 2, 2026

Installation

$ npx skills add microck/gsap-skills --skill gsap-scrolltrigger

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from microck/gsap-skills · top by installs.

npx skills add microck/gsap-skills

Browse all from microck/gsap-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch master
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,236 B
  • docs SUMMARY.md 189 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 11 installs

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 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 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

  1. Use scrub wisely - Good for visual control, bad for performance with many elements
  2. Pin only when necessary - Pinning adds complexity
  3. Leverage toggle actions - More performant than scrub for simple effects
  4. Refresh after DOM changes - Critical for dynamic content
  5. Use markers for development - Remove in production
  6. Combine with timelines - Easier to control complex sequences
  7. 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'