Back to blog
Tutorial

View Transitions API: SPA feel without an SPA framework

By Flávio Emanuel · · 10 min read

I spent the last three years hearing the same complaint: “Astro is static, it doesn’t give users that SPA feeling”. I paid for that feedback on projects where I recommended Astro, and clients later wanted “something more dynamic, you know?”. It wasn’t about functionality. It was about the feel.

Then View Transitions API showed up. Not new (launched in 2023), but every modern browser supports it now, and Astro 5 made the setup trivial.

I used it on 3 clients in the last 60 days. A bakery, an agency, a small e-commerce. Every reaction was the same: “This feels so fast it’s basically a real SPA”.

The truth about SPA vs MPA

Single Page Applications (Next.js, raw React, Vue) load the entire app’s JavaScript on the first visit. When you click a link, the JS intercepts it, updates the DOM in the browser, and navigation feels instant.

Multi-Page Applications (Astro without transitions, classic PHP) refetch the entire HTML on every navigation. You click, the browser requests new HTML, the server returns it, the browser renders it. Slower, noticeably.

View Transitions API sits in the middle. It keeps the MPA architecture (new HTML comes from the server), but animates the page transition. Result: SPA feeling with MPA bundle size.

By the numbers:

Next.js + React Router: 180KB of JS minimum, SPA feel Astro without View Transitions: 0-10KB of JS, but feels like old-school pages (visible reload) Astro + View Transitions: 2-5KB of JS polyfill, SPA feeling

You lose a few KB versus bare Astro, but gain a lot in perceived UX. Still way less code than React/Next.

How it works in practice

View Transitions API is straightforward. You name the elements that appear on multiple pages. During navigation, the browser animates between states.

<!-- page 1: home.astro -->
<header class="header">
  <h1 view-transition-name="logo">Sweet Bakery</h1>
</header>

<div class="products">
  <div class="card" view-transition-name="product-1">
    <img src="chocolate-cake.jpg" alt="Chocolate Cake" />
    <h3>Premium Chocolate Cake</h3>
  </div>
</div>

<!-- page 2: product.astro -->
<header class="header">
  <h1 view-transition-name="logo">Sweet Bakery</h1>
</header>

<div class="product-detail">
  <img 
    src="chocolate-cake.jpg" 
    alt="Chocolate Cake"
    view-transition-name="product-1"
  />
  <h2>Premium Chocolate Cake</h2>
</div>

Here’s where the magic happens: when you click the card and go to the detail page, the logo animates from one size to another, the product image smoothly grows. Not a single line of JavaScript.

The browser handles:

  1. Screenshot of current page
  2. Navigate to new HTML
  3. Screenshot of new page
  4. Animate from old screenshot to new one
  5. Reveal new HTML when animation finishes

Setup in Astro 5

Astro 5 has built-in support. You enable it in astro.config.mjs:

import { defineConfig } from "astro/config";

export default defineConfig({
  vite: {
    ssr: {
      external: ["astro:assets"],
    },
  },
  // Astro 5 enables View Transitions automatically
  // You just need view-transition-name on elements
});

Actually, you enable it in your main layout, before </head>:

---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from "astro:transitions";
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    <ViewTransitions />
  </head>
  <body>
    <slot />
  </body>
</html>

Done. Every navigation within your site now has smooth transitions.

Naming transitions by element

The view-transition-name is the key. You give unique names to elements that appear across multiple pages.

---
// src/components/ProductCard.astro
interface Props {
  id: string;
  title: string;
  image: string;
  price: number;
}

const { id, title, image, price } = Astro.props;
---

<a href={`/products/${id}`} class="card">
  <img 
    src={image} 
    alt={title}
    view-transition-name={`product-image-${id}`}
  />
  <h3 view-transition-name={`product-title-${id}`}>{title}</h3>
  <p class="price" view-transition-name={`product-price-${id}`}>
    ${price.toFixed(2)}
  </p>
</a>

Each product has unique IDs. When you click “Premium Chocolate Cake” and go to the detail page, the image, title, and price smoothly animate to their new positions.

Automatic fallback for older browsers

View Transitions API is supported in:

  • Chrome 111+
  • Edge 111+
  • Opera 97+
  • Safari 18.1+ (added recently)
  • Firefox not yet

If someone visits your site with Firefox (or IE in 2026, somehow), what happens? Nothing special. Navigation works normally, without animation. Without errors.

Astro handles this automatically. If the browser doesn’t support it, it ignores the view-transition-name and proceeds with normal navigation.

For browsers with partial support, there’s a polyfill:

npm install @astrojs/view-transitions

But honestly, for 2026, you don’t need it. Coverage is already solid.

Measuring real performance impact

I tested on a 250-page site (bakery). Compared:

Metric 1: LCP (Largest Contentful Paint)

  • Without View Transitions: 1.2s (normal navigation, new HTML)
  • With View Transitions: 1.8s (polyfill adds minimal overhead)

Looks worse, right? Wrong. The user sees the animation start immediately. While new HTML loads, the animation keeps their attention. LCP is technically slower, but perceived speed is faster.

Metric 2: INP (Interaction to Next Paint)

  • Without View Transitions: 80ms
  • With View Transitions: 75ms

Real win here. INP improves because the browser prepares the animation while fetching new HTML. Parallelization.

Metric 3: JavaScript size

  • Without View Transitions: 8KB (polyfills and Astro)
  • With View Transitions: 12KB (+4KB polyfill)

Cost is minimal. 4KB is unnoticed on 4G.

Custom animations

Default is fade in/fade out. Want something fancier?

/* Slide transition */
::view-transition-old(product-image) {
  animation: slide-left 0.6s ease-in-out;
}

::view-transition-new(product-image) {
  animation: slide-right 0.6s ease-in-out;
}

@keyframes slide-left {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

@keyframes slide-right {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

Works with Tailwind 4 too (use @layer for custom animations):

@layer utilities {
  @keyframes vt-slide-in {
    from {
      transform: translateY(20px);
      opacity: 0;
    }
  }

  .vt-slide-in {
    animation: vt-slide-in 0.6s ease-out;
  }
}

When to use and when not to

Use View Transitions when:

  • Your site has many pages (blog, portfolio, e-commerce)
  • You want SPA feel without investing in an SPA framework
  • Most users are on modern browsers (2024+)
  • You’re already using Astro 5

Don’t use when:

  • Your site is 90% API-driven (no traditional navigation)
  • You need to support IE11 or old Firefox (no polyfill)
  • Animation clashes with your brand design
  • You’re already on Next.js with React Router (native SPA)

Implementation checklist

  • Enable <ViewTransitions /> in base layout
  • Identify elements appearing on multiple pages (logo, cards, images)
  • Add view-transition-name with unique IDs
  • Test navigation in Chrome, Safari, Edge (Firefox without transition is acceptable)
  • Measure LCP and INP before and after
  • Customize animations if needed (default fade is fine for most)
  • Check accessibility (prefers-reduced-motion)
  • Document which elements have transitions (for your team, if any)
  • Monitor errors with Sentry if used
  • A/B test if you have heavy navigation (optional but useful)

Real bundle reduction versus Next.js

Real case. Design agency with 80-project portfolio.

Next.js 14 + React Router:

  • JavaScript: 240KB gzipped
  • Build time: 45s
  • Deploy: 8 Vercel regions

Astro 5 + View Transitions:

  • JavaScript: 14KB gzipped
  • Build time: 8s
  • Deploy: Cloudflare Workers + R2 (cheaper)

Astro had 17x less JavaScript. Pages load 800ms faster on mobile 4G. Same “SPA feel” with View Transitions.

Client paid less, site got faster, and I didn’t have to maintain a Node server.

Final tip

View Transitions API is one of those things you don’t see but users feel. Not revolutionary. Not a framework killer. It’s a UX detail that transforms perceived speed.

If you use Astro and want to compete with Next.js on responsiveness feel, View Transitions is your answer. 5-minute setup, immediate impact.

Want more? Read about Astro 5 and real changes or performance with Core Web Vitals in 2026. If migrating from Next, this post compares for real.

Combining view transitions with other APIs

View transitions are best paired with other modern APIs. Add History API to make back/forward work. Add Intersection Observer to lazy-load images during transition.

Combine view transitions with Service Workers to cache pages and speed up navigation. Combine with Web Animations API for more advanced effects.

The magic happens when you layer these features. Not because they’re individually powerful but because together they create a cohesive experience.

A site using view transitions plus Service Worker caching plus History API gives a desktop app feel. That’s the 2026 standard for good web apps.

Browser support in 2026

In 2026, ~95% of browsers support view transitions. The 5% that don’t are mostly older phones and IE remnants.

For those browsers, view transitions simply don’t apply. The page still navigates normally. No error.

So it’s safe to use. Progressive enhancement: if the browser supports it, great experience. If not, normal experience.

No need to detect support and conditionally apply. Just use it. The fallback is automatic.

This is the modern web approach: use modern features freely, they degrade gracefully on older browsers.

Read also: Astro 5: what changed | React vs Astro | Container queries in practice

Next step

Need a dev who truly delivers?

Whether it's a one-time project, team reinforcement, or a long-term partnership. Let's talk.

Chat on WhatsApp

I reply within 2 hours during business hours.