Docs · 11 pages
Docs / Patterns
View transitions
How every component survives ClientRouter navigation, and how to keep GSAP and Lenis working too.
<ClientRouter /> swaps the DOM without a reload and skips bundled scripts
that already ran
(Astro docs).
Anything set up on the first page now points at detached nodes; anything not
set up again on the next page does nothing. Five threads on GSAP’s forum
between April and August 2024 report it for ScrollTrigger and ScrollSmoother
(listed in the GSAP guide),
and Flowbite’s carousel stops working after a back navigation
(themesberg/flowbite#864,
open since April 2024).
What the library does
Every scripted component binds through the runtime: it
queries all instances on astro:page-load, tears down on astro:before-swap,
never double-binds, and never listens to astro:page-load alone (it does not
fire without the router). CSS-only components need nothing: fresh elements
restart their own animations.
On every swap the router gives <html> the next page’s attributes, which
drops data-ma-js, the gate entrance effects hide behind. The integration’s
gate script sets it again after every swap, and the runtime records it before
the swap and restores it after. Without that, every reveal after the first
navigation would either flash or never play.
All of this is verified in CI on Astro 5, 6 and 7 by navigating a real browser between pages and counting bindings.
GSAP and ScrollTrigger
GSAP already has the right tool for this: gsap.context() and revert().
The adapter calls them at the right moment.
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 });
ScrollTrigger.create({ trigger: '.pinned', pin: true, start: 'top top', end: '+=600' });
}, { scrollTrigger: ScrollTrigger });gsapPage runs setup inside a context once per page view, reverts it
(tweens, triggers, pin-spacers, inline styles) before the next swap, and calls
ScrollTrigger.refresh() on the frame after setup and again when web fonts
settle. Put it in a layout script so it registers once.
Lenis
import Lenis from 'lenis';
import { lenisPage } from '@moonarc/core/lenis';
const lenis = lenisPage(() => new Lenis(), { gsap, scrollTrigger: ScrollTrigger });One instance for the life of the document (the document persists across
navigations, so must the scroller), driven by gsap.ticker when given. After
every swap it stops an animation still running from the previous page,
resyncs Lenis to the position the router set, and writes Lenis’ classes on
<html> again, which the swap replaced. Without that stop, a link clicked
or the back button pressed while Lenis is still easing carries the next page to
the old page’s target: 1200 px down instead of the top, measured with Lenis
1.3.26. The Lenis guide
has the details.
Your own scripts
Use onMount and onPage from the runtime instead of astro:page-load
listeners. See runtime.
Page transitions themselves
transition:name, transition:animate and transition:persist are Astro’s
own. A component inside an element kept by transition:persist is released
before the swap and bound again after it, like any other: the element, its
DOM and its media survive, and the component’s setup runs once more on it.
Keep page-level transitions between 150 and 300 ms; anything longer sits in
front of the user’s intent.