Astro View Transitions: A Practical Tutorial
Learn how to add smooth, native page transitions to your Astro site using the View Transitions API with practical examples and gotchas.
What you'll learn
- ✓What the View Transitions API actually does
- ✓How to enable transitions in Astro with one import
- ✓How to persist elements across navigations
- ✓How to name shared elements for morph effects
- ✓Common pitfalls and accessibility tips
Prerequisites
- •HTML basics
- •Astro project setup
What and Why
Astro’s <ViewTransitions /> component wires up the browser’s native View Transitions API to make multi-page sites feel like single-page apps. Instead of the jarring white flash between pages, you get a smooth cross-fade, slide, or custom morph. The best part: it works on regular MPA navigations, so you keep Astro’s fast static output without rewriting your site as a SPA.
Why bother? Because the perception of speed matters as much as actual speed. A 200 ms cross-fade hides the small delay of fetching the next page, and shared element transitions make navigations feel intentional rather than abrupt.
Mental Model
Think of view transitions like a stage crew between scenes. When you click a link, the browser snapshots the current page, fetches the next one, then animates from the old snapshot to the new one. Astro handles the orchestration: it intercepts the click, swaps the <head> and <body>, fires lifecycle events, and lets the browser do the morph.
You can mark specific elements with a transition:name so the browser knows “this image on page A is the same as that image on page B” and animates between them instead of cross-fading.
Hands-on Example
Add the component to your shared layout:
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
That single import is enough to get a cross-fade between every page. To create a shared element morph, name the element on both pages:
<!-- /blog/index.astro -->
<a href="/blog/hello">
<img src="/hero.jpg" transition:name="hero-img" />
</a>
<!-- /blog/hello.astro -->
<img src="/hero.jpg" transition:name="hero-img" />
The browser now morphs the image from its grid position to the article header.
Click link
|
v
[snapshot old page] -- browser
|
v
[fetch next page] -- astro
|
v
[swap head + body] -- astro
|
v
[run morph anim] -- browser
|
v
astro:page-load fires You can also persist a component, like a video player or audio, across navigations:
<video transition:persist controls src="/intro.mp4" />
The transition:persist directive keeps the DOM node alive so playback continues seamlessly between pages.
Common Pitfalls
- Re-running scripts. Scripts in the body do not re-execute on every navigation. Listen for
astro:page-loadinstead ofDOMContentLoaded. - Third-party widgets. Analytics or chat widgets often assume a full page load. Re-initialize them in an
astro:page-loadhandler. - Naming collisions. Two elements on the same page sharing a
transition:namewill break the morph. Names must be unique per page. - Forgetting fallback. Browsers without View Transitions support fall back to a normal navigation, but custom animations need a
transition:animateor CSS guard. - Accessibility. Honor
prefers-reduced-motionand avoid long, distracting animations.
Best Practices
- Put
<ViewTransitions />in your root layout so every page benefits. - Use
transition:persistsparingly. Persisting heavy components defeats the purpose of fresh navigations. - Name shared elements consistently across templates so morphs work both ways.
- Wrap analytics initialization in
document.addEventListener('astro:page-load', ...). - Test with reduced-motion enabled and confirm the animations downgrade gracefully.
- Keep transition durations under 300 ms. Anything longer feels sluggish.
Wrap-up
Astro view transitions give you SPA-like polish without the SPA cost. A single import gets you smooth cross-fades; a few directives unlock shared element morphs and persisted media. The trick is remembering that scripts re-run on every navigation differently than before, so wiring lifecycle events correctly matters. Once you have that down, your site goes from feeling like a stack of HTML files to feeling like a connected experience, with no framework rewrite required.
Related articles
- Astro Astro Content Collections Tutorial
Build a typed blog with Astro content collections, including Zod schemas, references, and dynamic routes generated from Markdown files.
- Astro Astro Image Component and Optimization
Master Astro's built-in Image and Picture components to ship responsive, lazy-loaded, modern-format images without external services.
- Astro Astro Integrations and Adapters: An Overview
Understand the difference between Astro integrations and adapters, when to reach for each, and how they shape your deployment pipeline.
- Astro Astro Islands Architecture Explained
Learn how Astro ships zero JavaScript by default and only hydrates the interactive components you mark as islands.