Skip to content

Adding View Transitions to a Multi-Page Astro Site

The View Transitions API gives multi-page apps SPA-like page animations with a few lines of CSS, no client-side router or JavaScript framework required.

· · 4 min read

Quick Take

I assumed smooth page transitions meant committing to a client-side router. Then I added three lines of CSS to a plain multi-page Astro site and got the same effect, no router, no JavaScript.

Every "smooth page transition" tutorial I'd read assumed a single-page app with a client-side router intercepting navigation. My site is a plain multi-page Astro build, full page loads, no router. I didn't think that combination could have transitions at all, until I found the cross-document View Transitions API is built for exactly this case.

TL;DR: The cross-document View Transitions API animates full page navigations in a multi-page app using a single @view-transition { navigation: auto; } CSS rule, no JavaScript router required. The browser captures a screenshot of the old and new page and cross-fades or animates between them. Tag specific elements with view-transition-name to get individual shared-element transitions, like a hero image morphing between pages instead of everything cross-fading together.

The One Rule That Turns It On

/* global.css, loaded on every page */
@view-transition {
  navigation: auto;
}

That's the entire opt-in. With this rule present, every same-origin navigation on the site gets a default cross-fade transition between the old and new page, no other code required. Browsers that don't support the API ignore the unknown at-rule and navigate normally, so there's no fallback branch to write.

Customizing the Default Cross-Fade

The default transition is a plain cross-fade, which is already an improvement over an instant page swap, but real design systems usually want something more specific:

::view-transition-old(root) {
  animation: 200ms ease-out both fade-out;
}

::view-transition-new(root) {
  animation: 200ms ease-in both fade-in;
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
}

::view-transition-old(root) and ::view-transition-new(root) are browser-generated pseudo-elements representing screenshots of the outgoing and incoming page. You're animating two static images, not the live DOM, which is why this stays fast even on a page with a lot of content, the browser isn't re-rendering anything during the animation.

Shared-Element Transitions With view-transition-name

The more striking effect is a single element, a hero image, a page title, appearing to smoothly move and resize between two pages instead of just fading. That needs a matching view-transition-name on both pages:

/* On the article list page */
.article-card .thumbnail {
  view-transition-name: article-hero;
}
/* On the individual article page */
.article-hero-image {
  view-transition-name: article-hero;
}
<!-- article-list.html -->
<a href="/blog/my-post/">
  <img class="thumbnail" src="/thumb.webp" alt="..." />
</a>

<!-- article page -->
<img class="article-hero-image" src="/hero.webp" alt="..." />

When the user clicks from the list to the article, the browser sees both elements share the name article-hero and animates the thumbnail growing into the hero image's position and size, instead of cross-fading two unrelated images. The visual effect reads as one image moving, not two images swapping.

view-transition-name must be unique per page at any given moment, reusing the same name on two elements on the same page is invalid and the browser drops the transition for both.

Respecting prefers-reduced-motion

Per the portfolio's design principles, motion needs an opt-out for users who've asked for reduced motion at the OS level. The View Transitions API respects prefers-reduced-motion automatically for its default cross-fade in most browsers, but custom @keyframes animations you write yourself need an explicit guard:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

Without this, a custom slide or scale animation you wrote keeps running for users who've explicitly asked their OS to minimize motion, defeating the purpose of the setting.

Scoping Transitions to Specific Navigations

Not every navigation should transition the same way, or at all. The navigation CSS property inside @view-transition only has one meaningful value today (auto), but you can scope which pages participate by conditionally including the transition CSS, or by checking the navigation type in a small script if you need per-route control:

// Skip the transition for external or same-page anchor navigations
document.addEventListener('pagereveal', (event) => {
  if (!event.viewTransition) {return;}
  const navigationType = navigation.activation?.navigationType;
  if (navigationType === 'reload') {
    event.viewTransition.skipTransition();
  }
});

skipTransition() cancels the animation for that specific navigation while leaving the CSS rule active for every other one, useful for reloads or navigations where an animated swap would look wrong (jumping to a same-page anchor, for instance).

Old Way vs Modern Way

TaskBeforeWith cross-document View Transitions
Smooth page-to-page fadeClient-side router + JS-driven fadeOne @view-transition CSS rule
Shared hero image between pagesSPA framework animation libraryview-transition-name on both pages
Fallback for unsupported browsersManual feature detectionNone needed, unsupported browsers just navigate normally

Conclusion

The assumption that smooth page transitions require a single-page app and a client-side router doesn't hold anymore. The cross-document View Transitions API brings the same effect to a plain multi-page site with a CSS rule and, for shared elements, a matching view-transition-name. If your site already ships full page loads and you've been eyeing an SPA rewrite mainly for the transitions, this closes that gap without the rewrite.

Frequently Asked Questions

Does the View Transitions API require a JavaScript framework?
No. The cross-document version of the API, the one that animates full page navigations in a multi-page app, works with plain HTML and CSS, you opt in with a single @view-transition rule and the browser handles capturing the before/after screenshots and animating between them. A JavaScript-driven version also exists for single-page apps that swap DOM content without a full navigation, but MPAs don't need it.
What happens in browsers that don't support the View Transitions API?
Nothing breaks. The API is progressive enhancement by design, unsupported browsers simply navigate normally with no transition animation, the same instant page swap they'd get anyway. There's no polyfill needed and no fallback code required, you just don't get the animation in that browser.
Can I animate individual elements differently during a page transition?
Yes, using view-transition-name. Any element you tag with a unique view-transition-name value gets its own transition, tracked across the navigation, which is how a shared hero image or title can appear to smoothly move and resize between two pages instead of just cross-fading with everything else.