# 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](https://docs.astro.build/en/guides/view-transitions/#script-re-execution)).
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](/resources/guides/gsap-scrolltrigger-astro-view-transitions/)),
and Flowbite's carousel stops working after a back navigation
([themesberg/flowbite#864](https://github.com/themesberg/flowbite/issues/864),
open since April 2024).

## What the library does

Every scripted component binds through the [runtime](/docs/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.

```ts
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

```ts
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](/resources/guides/lenis-smooth-scroll-astro-clientrouter/)
has the details.

## Your own scripts

Use `onMount` and `onPage` from the runtime instead of `astro:page-load`
listeners. See [runtime](/docs/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.