← All articles

[ Blog ]

June 24, 2026

4 min read

GSAP ScrollTrigger Tutorial: Pin, Scrub & Parallax

Learn GSAP ScrollTrigger by building the three patterns behind award-winning sites — pinned sections, scrubbed timelines, and parallax — in React/Next.js.

GSAPScrollTriggerReactAnimationNext.js
GSAP ScrollTrigger Tutorial: Pin, Scrub & Parallax

GSAP ScrollTrigger is the single most useful tool for scroll-driven motion on the web. Once you understand three patterns — pin, scrub, and parallax — you can recreate most of the scroll effects you see on award-winning sites. This GSAP ScrollTrigger tutorial builds all three in React/Next.js, and explains the start/end syntax that trips everyone up.

If you also want buttery scrolling underneath these effects, pair this with smooth scroll using Lenis — the two are designed to work together.

Setup

npm install gsap @gsap/react

In React, always animate inside useGSAP so tweens are scoped and cleaned up automatically:

'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(useGSAP, ScrollTrigger);

Understanding start and end

Every ScrollTrigger has a start and end, each written as "[trigger] [scroller]":

  • "top bottom" → when the top of the trigger hits the bottom of the viewport.
  • "top center" → top of the trigger reaches the vertical center.
  • "bottom top" → bottom of the trigger reaches the top of the viewport.

You can also use offsets and relative values: start: 'top 80%', end: '+=600' (600px of scroll after the start). Add markers: true while developing — it draws the trigger positions on screen and removes 90% of the guesswork.

Pattern 1 — Pin a section

Pinning fixes an element in place while the page keeps scrolling — the foundation of "sticky" storytelling sections.

function Pinned() {
  const ref = useRef<HTMLDivElement>(null);
  useGSAP(() => {
    ScrollTrigger.create({
      trigger: ref.current,
      start: 'top top',
      end: '+=100%',   // stay pinned for one viewport of scroll
      pin: true,
    });
  }, { scope: ref });

  return <section ref={ref} className="h-screen">Pinned content</section>;
}

end: '+=100%' keeps it pinned for one extra viewport height. Increase it for a longer "hold".

Pattern 2 — Scrub a timeline

scrub ties an animation's progress to the scrollbar, so the user scrubs the motion by scrolling. Combine it with pin and you get the classic pinned-and-animated section.

function ScrubReveal() {
  const ref = useRef<HTMLDivElement>(null);
  useGSAP(() => {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: ref.current,
        start: 'top top',
        end: '+=150%',
        scrub: 1,     // 1 = smooth catch-up; true = locked to scroll
        pin: true,
      },
    });
    tl.from('.headline', { yPercent: 100, opacity: 0 })
      .to('.bg', { scale: 1.2, ease: 'none' }, 0);
  }, { scope: ref });

  return <section ref={ref}>{/* .headline, .bg */}</section>;
}

scrub: 1 adds a one-second smoothing so the animation glides instead of snapping — this is the trick that makes scrubbed sections feel premium.

Pattern 3 — Parallax

Parallax just means layers moving at different speeds. With scrub, it's a one-liner per layer:

useGSAP(() => {
  gsap.to('.layer-back', {
    yPercent: -30,
    ease: 'none',
    scrollTrigger: { trigger: '.scene', start: 'top bottom', end: 'bottom top', scrub: true },
  });
}, { scope: ref });

Move the background layer less than the foreground (-30 vs -60) and the depth illusion appears. Always parallax with yPercent/transform — never top/margin — so it stays on the compositor.

Performance & cleanup

A few rules keep ScrollTrigger smooth and bug-free:

DoWhy
Animate transform / opacity onlyStays on the GPU compositor; no layout thrash
Use useGSAP({ scope })Auto-reverts tweens + triggers on unmount (no leaks)
Call ScrollTrigger.refresh() after layout shiftsFonts/images change element positions
Set invalidateOnRefresh: true for vw/vh valuesRecomputes on resize

In the App Router, ScrollTrigger lives in client components only. Register the plugin once, and let useGSAP's scope handle cleanup across route changes.

FAQ

When should I use scrub: true vs scrub: 1?

true locks the animation 1:1 to the scrollbar (precise, can feel rigid). A number adds that many seconds of smoothing (scrub: 1 is the sweet spot for most reveals).

Why is my pinned section jumping?

Almost always a layout shift after ScrollTrigger measured the page (late fonts, images without dimensions, or a smooth-scroll library not wired in). Set image sizes and call ScrollTrigger.refresh() once things settle.

Does ScrollTrigger work with smooth scroll?

Yes — drive both from one RAF loop and call ScrollTrigger.update() each frame. See the smooth scroll guide for the exact setup.


Pin, scrub, and parallax are the three primitives behind almost every scroll experience worth copying. Master the start/end syntax, keep everything on transform, and you can build scroll stories that feel hand-crafted. Want this level of motion in your product? See how I work with teams.