GSAPNext.jsAnimationFrontend

GSAP ScrollTrigger in Next.js App Router: A Practical Guide

Pinned sections, horizontal scroll, and scrub animations that survive React strict mode, hydration, and route changes.

The three ways ScrollTrigger breaks in Next.js

GSAP and React have an uneasy relationship. In the App Router you will hit all three of these:

  1. Strict mode double-invocation creates duplicate triggers.
  2. Hydration measures layouts before fonts and images settle.
  3. Route changes leave orphaned pinned spacers in the DOM.

The pattern that fixes all three

Wrap everything in gsap.context() inside useLayoutEffect, and revert it on cleanup:

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(track, {
      x: -total,
      ease: "none",
      scrollTrigger: {
        trigger: section,
        scrub: 1,
        pin: true,
        end: () => `+=${total}`,
        invalidateOnRefresh: true,
      },
    });
  }, section);
  return () => ctx.revert();
}, []);

ctx.revert() kills every trigger and undoes every inline style GSAP wrote — no orphans, no doubles.

Measuring after things settle

Use a function for end and set invalidateOnRefresh: true so measurements re-run on refresh. For image-heavy tracks, call ScrollTrigger.refresh() once images load.

Respect reduced motion

A pinned horizontal scroll is a vestibular nightmare for some users. Check prefers-reduced-motion and fall back to a static grid — your animation should be progressive enhancement, not a requirement.