View Transitions
Astro’s view transitions provide SPA-like page navigation without a client-side router or framework runtime. Pages load as full HTML documents but transition smoothly with animations.
Setup
Add the <ViewTransitions /> component to your layout’s <head>:
---
// src/layouts/Base.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
That is it. Internal navigation now uses the browser’s View Transitions API with a fallback for browsers that do not support it.
What happens
- User clicks an internal link
- Astro intercepts the click
- Fetches the new page as HTML
- Applies a crossfade transition between old and new content
- Updates the URL and browser history
No full page reload. No client-side router. No framework runtime required.
Transition directives
transition:name
Match elements across pages for morph animations:
<!-- Page 1: blog list -->
<img src={post.image} transition:name={`hero-${post.id}`} />
<!-- Page 2: blog post -->
<img src={post.image} transition:name={`hero-${post.id}`} />
The image morphs from its position on the list page to its position on the post page.
transition:animate
Control the animation type:
<div transition:animate="slide">Slides in</div>
<div transition:animate="fade">Fades in</div>
<div transition:animate="none">No animation</div>
Built-in animations: fade (default), slide, none, initial.
transition:persist
Keep an element alive across navigations (useful for audio players, video, iframes):
<audio id="player" transition:persist>
<source src="/podcast.mp3" />
</audio>
The audio player continues playing as the user navigates between pages.
Client islands with transition:persist keep their state:
<MusicPlayer client:load transition:persist />
Custom animations
Define custom animations with CSS:
---
import { fade } from 'astro:transitions';
---
<div transition:animate={fade({ duration: '0.5s' })}>
Custom fade duration
</div>
Or use full CSS keyframes:
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
<div transition:animate={{
old: { name: 'fadeOut', duration: '0.2s' },
new: { name: 'slideUp', duration: '0.3s' },
}}>
Custom transition
</div>
Lifecycle events
Listen for transition events in client scripts:
<script>
document.addEventListener('astro:before-preparation', (event) => {
// Before the new page is fetched
});
document.addEventListener('astro:after-swap', (event) => {
// After the new page content is swapped in
// Re-initialize scripts, analytics, etc.
});
document.addEventListener('astro:page-load', (event) => {
// After everything is complete
// Runs on initial page load AND after transitions
});
</script>
Prefetching
Astro can prefetch links on hover or when they enter the viewport:
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true, // prefetch all links
defaultStrategy: 'hover', // or 'viewport', 'load', 'tap'
},
});
Or per-link:
<a href="/about" data-astro-prefetch="hover">About</a>
<a href="/contact" data-astro-prefetch="viewport">Contact</a>
Comparison with Next.js navigation
| Feature | Next.js | Astro |
|---|---|---|
| Client-side navigation | React router (automatic) | View Transitions API |
| JS required | Yes (React runtime) | Minimal (transition script only) |
| Element morphing | Not built-in | transition:name |
| Persist elements | Not built-in | transition:persist |
| Prefetching | Automatic for <Link> | Configurable strategies |
| Fallback for old browsers | N/A (always uses React) | Graceful degradation to full page load |