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

  1. User clicks an internal link
  2. Astro intercepts the click
  3. Fetches the new page as HTML
  4. Applies a crossfade transition between old and new content
  5. 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

FeatureNext.jsAstro
Client-side navigationReact router (automatic)View Transitions API
JS requiredYes (React runtime)Minimal (transition script only)
Element morphingNot built-intransition:name
Persist elementsNot built-intransition:persist
PrefetchingAutomatic for <Link>Configurable strategies
Fallback for old browsersN/A (always uses React)Graceful degradation to full page load