View Transitions
Multi-page Astro sites are fast precisely because each navigation is a full page load with zero JavaScript by default. The trade-off is the visible flash between pages that single-page apps avoid. Astro closes that gap with the ClientRouter component, which intercepts navigation, swaps the DOM in place, and animates the change using the browser’s native View Transitions API. You get SPA-like smoothness while keeping the multi-page architecture — no client-side router framework required.
Enabling the ClientRouter
The whole feature is opt-in through a single component imported from astro:transitions and rendered inside your <head>. Add it to a shared layout so every page that uses the layout participates in transitions.
---
import { ClientRouter } from "astro:transitions";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DevCraftly</title>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
With ClientRouter present, Astro hijacks same-origin link clicks and history navigation. Instead of a hard reload, it fetches the next page, diffs the <head>, and replaces the <body> while playing a transition. On browsers without the View Transitions API, it falls back gracefully to a standard full-page navigation — your site keeps working everywhere.
ClientRouterwas namedViewTransitionsbefore Astro 4.5. If you seeimport { ViewTransitions }in older guides, the current, correct name isClientRouter.
Default and custom animations
Out of the box, navigation uses a subtle fade. You can switch the animation per element with the transition:animate directive, which accepts a built-in name or a custom configuration.
<main transition:animate="slide">
<slot />
</main>
The built-in values are summarized below.
| Value | Effect |
|---|---|
fade | Old content fades out, new content fades in (the default) |
slide | Old content slides out left, new slides in from the right |
none | Disables Astro’s animation for this element |
initial | Uses the browser’s default cross-fade with no Astro styling |
You can also build a fully custom animation by importing helpers and passing an object:
---
import { fade } from "astro:transitions";
---
<header transition:animate={fade({ duration: "0.4s" })}>
<slot name="header" />
</header>
Persisting state and identity across pages
Two directives do the heavy lifting for app-like continuity: transition:name and transition:persist.
transition:name gives an element a stable identity so the browser morphs it from one page to the next — ideal for a thumbnail that grows into a hero image. The name must be unique on each page.
---
const { post } = Astro.props;
---
<a href={`/blog/${post.slug}`}>
<img src={post.cover} alt={post.title} transition:name={`cover-${post.slug}`} />
</a>
On the destination page, reuse the same name on the larger image and the browser animates between the two automatically.
transition:persist keeps a component or element alive across the navigation instead of recreating it. This is what lets an audio player keep playing or a sidebar keep its scroll position.
<aside transition:persist>
<nav><!-- scroll position survives navigation --></nav>
</aside>
Persisted islands keep their state too. A
<Counter client:load transition:persist />will retain its count as the user moves between pages, because Astro reuses the existing DOM node and its hydration state.
Reacting to navigation with lifecycle events
Because the page no longer reloads, scripts that ran on DOMContentLoaded won’t re-run on every navigation. Astro emits lifecycle events on document so you can re-initialize logic at the right moment.
document.addEventListener("astro:page-load", () => {
// Runs after the initial load AND after every transition.
console.log("Page is ready:", window.location.pathname);
});
document.addEventListener("astro:after-swap", () => {
// Runs immediately after the new DOM is swapped in, before paint.
document.documentElement.dataset.theme = localStorage.theme ?? "light";
});
Output:
Page is ready: /
Page is ready: /blog/view-transitions
The key events are astro:before-preparation, astro:after-preparation, astro:before-swap, astro:after-swap, and astro:page-load. Prefer astro:page-load for setup that must run on every page, since it fires on the first load and after each transition.
Programmatic navigation
For navigation triggered by code — after a form submit, say — use the navigate helper instead of setting window.location, so the transition still plays.
import { navigate } from "astro:transitions/client";
async function onSubmit(event: SubmitEvent) {
event.preventDefault();
await saveDraft();
navigate("/dashboard");
}
Best practices
- Add
ClientRouteronce in a shared layout rather than per page, so transitions are consistent site-wide. - Reach for
transition:nameto create morphing hero/thumbnail effects, and keep names unique per page. - Use
transition:persistfor stateful islands (players, menus) so they survive navigation instead of remounting. - Move per-page initialization from
DOMContentLoadedtoastro:page-load, or it will only run once. - Keep animation durations short (200-400ms); long transitions feel sluggish on fast multi-page sites.
- Test with the API disabled or in an unsupporting browser to confirm the full-reload fallback behaves correctly.
- Respect
prefers-reduced-motion— Astro honors it automatically, so avoid overriding it with forced custom animations.