Back to postsArnold Lupamo
Feb 3, 2025·8 min read

Understanding GSAP ScrollTrigger

ScrollTrigger is one of the most powerful animation tools available. Here's how I actually think about it when building scroll-driven UIs.


ScrollTrigger confused me the first time I used it. Not because the API is bad — it's excellent — but because the mental model is different from what you'd expect.

The core idea

A ScrollTrigger watches a trigger element and fires animation logic based on where that element sits relative to the scroller (usually the viewport). You describe a start and end scroll position, and everything in between is up to you.

gsap.to(element, {
  y: -100,
  scrollTrigger: {
    trigger: element,
    start: "top 80%",   // when element's top hits 80% from top of viewport
    end: "bottom 20%",
    scrub: 1,           // ties animation to scroll position
  },
});

Pin vs scrub

These two are often confused:

  • **`scrub`** ties the animation progress to scroll progress. `scrub: 1` adds a 1-second lag for smoothness.
  • **`pin`** freezes the element in place while the user scrolls past it — useful for horizontal scroll sections.
  • The gotcha with pinSpacing

    When you pin an element, GSAP adds space after it to prevent content jumping. If you're stacking pinned sections (like a hero that fades into the next section), you usually want `pinSpacing: false` so sections overlap correctly.

    Horizontal scroll pattern

    For a cards carousel, calculate the total scroll width and translate the track:

    const totalScroll = track.scrollWidth - window.innerWidth;
    gsap.to(track, {
      x: -totalScroll,
      ease: "none",
      scrollTrigger: {
        trigger: section,
        start: "top top",
        end: () => `+=${totalScroll}`,
        scrub: 1,
        pin: true,
      },
    });

    Use an arrow function for `end` so it recalculates on resize.