Skip to content
C Codeloom
Astro

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.

·4 min read · By Codeloom
Intermediate 8 min read

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
Lifecycle of an Astro view transition

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-load instead of DOMContentLoaded.
  • Third-party widgets. Analytics or chat widgets often assume a full page load. Re-initialize them in an astro:page-load handler.
  • Naming collisions. Two elements on the same page sharing a transition:name will 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:animate or CSS guard.
  • Accessibility. Honor prefers-reduced-motion and avoid long, distracting animations.

Best Practices

  • Put <ViewTransitions /> in your root layout so every page benefits.
  • Use transition:persist sparingly. 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.