Resources / Guide · 2026-09-12
GSAP ScrollTrigger with Astro View Transitions
Why ScrollTrigger stops working after a ClientRouter navigation, and the adapter that sets it up on every page and cleans up before the next one.
The symptom
Your ScrollTrigger animations work on the first page. You add
<ClientRouter /> for smooth navigation, click a link, and nothing on the next
page animates or pins. Sites without the router are not affected, because
every page is a full load that runs its scripts again.
Five threads on GSAP’s forum between April and August 2024 report it: ScrollTrigger breaking on a second visit, ScrollSmoother stopping after a navigation, pinning with ScrollSmoother, markers that need a reload and setting GSAP up with view transitions. A GreenSock administrator answers all five the same way: clean up the previous page’s instances and create new ones after each navigation.
Why it breaks
ClientRouter swaps the DOM without a reload, and Astro runs a bundled module
script only once per visit
(Astro docs).
Your gsap.from() and ScrollTrigger.create() calls ran once, against the
first page’s elements. We measured it with GSAP 3.15 on Astro 7.3.2 in
Chromium, Firefox and WebKit: after one navigation both triggers still exist,
neither points at an element on the page, and the new page has no pin and no
tween. Moving the setup into an astro:page-load listener makes every page
animate, but nothing kills the triggers of the page you left, so the count
goes 2, 4, 6, 8 over three navigations.
The fix
GSAP already has the right primitive. gsap.context() records everything
created inside it; revert() kills tweens and triggers, removes pin-spacers
and restores inline styles. The missing piece is calling them at the right
moments in Astro’s lifecycle. gsapPage does that:
// src/layouts/Base.astro — inside <script>
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { gsapPage } from '@moonarc/core/gsap';
gsap.registerPlugin(ScrollTrigger);
gsapPage(
gsap,
() => {
gsap.from('.hero h1', { y: 24, opacity: 0, duration: 0.7, ease: 'power3.out' });
ScrollTrigger.create({ trigger: '.pinned', pin: true, start: 'top top', end: '+=600' });
},
{ scrollTrigger: ScrollTrigger },
);What happens now, on every page view:
setupruns inside a freshgsap.context(), on the first load and after every navigation, exactly once each.ScrollTrigger.refresh()runs on the next frame, and again whendocument.fonts.readyresolves, so positions are measured against the real layout.- Before the next swap, the context is reverted: no leaked triggers, no
orphaned
pin-spacer, no inline styles left on elements about to be replaced.
Put it in a layout script so it registers once. It also works on pages without the router.
How the fix is tested
A browser test navigates between two pages with pinned sections on Astro 5, 6 and 7 and asserts: page A has exactly one trigger and one pin-spacer, page B exactly two and two, back to A exactly one and one, and the tween applied on every page.
Install
npx astro add moonarc
npm i gsapgsap is passed in, never bundled by us, so you keep one GSAP instance and
your plugin registrations. See view transitions.
Animation libraries for Astro, measured shows what a GSAP reveal set up once in a layout does after a
navigation, and what the same setup inside gsap.context() on every page does, beside AOS, Motion, AstroAnimate,
astro-reveal and Reveal.