React
View transitions in Astro: what persists and what reloads
The Astro router replaces the body on every navigation, so scripts run again. transition:persist and page-load fix most cases.
Key points
- ClientRouter intercepts internal links and replaces the body on every navigation.
- transition:persist keeps the DOM node, not JavaScript state.
- astro:page-load is the event to use instead of DOMContentLoaded.
View transitions in Astro are turned on by <ClientRouter /> in the layout: from then on every internal link is intercepted, the body is replaced and the browser animates the change. The cost is that nothing stays mounted unless you say so.
Turning the router on
---
import { ClientRouter } from "astro:transitions";
---
Up to Astro 4 the component was called ViewTransitions. The new name says what it does: it does not just use the browser API, it replaces navigation with a client-side router.
What survives a navigation
| Element | Default behaviour | Directive |
|---|---|---|
| Interactive island | Unmounts and remounts | None |
| Audio player | Stops | transition:persist |
| Open modal | Closes | transition:persist |
| Inline script | Runs again on every page | astro:page-load |
One shared name for the animation
---
const { posts } = Astro.props;
---
{posts.map((post) => (
))}
transition:name connects two elements that stand for the same thing on different pages: the browser animates position and size between them. The name must be unique on the departing page and on the arriving one, so it carries the post id and not the word cover.
Without transition:name the router still cross-fades the whole body: that is the default animation, and transition:animate replaces it when a different movement is needed.
Persisting an element
transition:persist keeps the DOM node when the element appears on the new page with the same identity. By default a persisted island receives the new props; transition:persist-props keeps the ones from the first render.
Initialising after each navigation
document.addEventListener("astro:page-load", () => {
document.querySelectorAll("[data-reveal]").forEach((el) => {
el.classList.add("is-visible");
});
});
A listener on DOMContentLoaded runs once: after the first navigation it is never called again. The right event is astro:page-load, which also fires on the first load.
When to break the interception
- A link to another origin: no interception, normal navigation.
- Downloads or PDFs: add
data-astro-reloadfor a full reload. - POST forms: they skip the router and the page reloads.
- Giving the same
transition:nameto two images on one page: the animation starts from the wrong element.
The known limitation
The router replaces the body, not JavaScript state. A counter inside an ES module starts from zero on every navigation, even with transition:persist: persistence covers the DOM, not the variables. If state has to survive across pages, put it in an explicit store or in sessionStorage.
The decision rule
Rule: add the router when navigation is frequent and the header stays identical between pages; on a three-page site the 38.0 KB does not pay for itself and a full load is more predictable. The second criterion is state: if the next page must remember what the user did, the router is not enough.
The cost also shows in debugging: an event bound to window survives navigation, a listener on a node does not, and the difference only appears after the second click. Keep a single initialisation point and call it from astro:page-load.
The router emits four events besides astro:page-load: astro:before-preparation, astro:after-preparation, astro:before-swap and astro:after-swap. They let you read the incoming URL before the DOM changes and restore an element after the swap, without waiting for the new page to be ready.
Commands
npx astro check
npx astro build
From the catalogue: viewfinder flip carousel, the Astro logo block and saastro. The guides: content collections and the missing client directive; the comparison: Astro against Next.js for documentation.
Frequently asked questions
Do I have to change my links?
No. The router intercepts the internal links it finds in the document, so plain anchors with an href are enough.
Why does my script stop running after the first page?
Because DOMContentLoaded does not repeat. Move the initialisation to astro:page-load.
Do view transitions work in Safari and Firefox?
Yes, with different levels of browser API support. Where it is missing, Astro navigates normally without animation.
How do I turn the animations off?
Remove ClientRouter from the layout and you are back to classic navigation. For a single link use the data-astro-reload attribute.
Can I use them with React islands?
Yes. The island unmounts and remounts on every navigation, unless you add transition:persist to the element that wraps it.