The View Transitions API: Build Smooth Page Transitions That Boost Perceived Performance

Learn how to implement the View Transitions API for smooth, GPU-accelerated page transitions in both SPAs and MPAs. Covers cross-document transitions, Speculation Rules integration, Core Web Vitals impact, and a full performance checklist.

Users expect native-app smoothness from the web. A hard page-refresh between routes feels jarring, breaks spatial context, and honestly makes even a fast site feel slow. The View Transitions API solves this at the platform level — letting the browser orchestrate GPU-accelerated animations between DOM states (or even between entirely different documents) with minimal code and zero third-party libraries.

In this guide, you'll learn how the API works under the hood, how to implement both same-document and cross-document transitions step by step, how to pair them with the Speculation Rules API for truly instant navigations, and how to avoid the performance pitfalls that can hurt your Core Web Vitals. So, let's dive in.

What Is the View Transitions API?

The View Transitions API is a browser-native mechanism for creating animated transitions between different views of a web page — or between different pages entirely. Instead of reaching for heavyweight JavaScript animation libraries, the API uses a declarative snapshot model:

  1. The browser captures a screenshot (snapshot) of the current state.
  2. Your DOM update runs while rendering is suppressed.
  3. The browser captures a snapshot of the new state.
  4. A CSS animation (by default, a cross-fade) interpolates between old and new snapshots.

Because the browser controls this entire pipeline, animations are composited on the GPU, off the main thread, and inherently more performant than any userland animation solution. It's one of those rare cases where the platform just does it better.

Browser Support in 2026

Same-document view transitions hit Baseline Newly Available status in October 2025 with Firefox 144. Here's the current support matrix:

Feature Chrome Edge Firefox Safari
Same-document transitions 111+ 111+ 133+ 18+
Cross-document transitions 126+ 126+ Not yet 18.2+
view-transition-class 125+ 125+ 144+ 18.2+
View transition types 125+ 125+ 147+ 18.2+

The best part? The API degrades gracefully. If the browser doesn't support it, your DOM updates still execute — they just won't animate. That makes it perfectly safe to adopt as a progressive enhancement today.

Same-Document Transitions (SPAs)

Same-document transitions are designed for single-page applications where you update the DOM without a full page navigation. Here's the minimal implementation.

Step 1: Trigger the Transition

function navigateTo(url) {
  // Fallback for unsupported browsers
  if (!document.startViewTransition) {
    updateDOM(url);
    return;
  }

  // Start the view transition
  const transition = document.startViewTransition(() => {
    updateDOM(url);
  });
}

async function updateDOM(url) {
  const response = await fetch(url);
  const html = await response.text();
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");
  document.querySelector("main").innerHTML =
    doc.querySelector("main").innerHTML;
}

The callback you pass to startViewTransition() is where the DOM mutation happens. The browser snapshots the old state before your callback runs and the new state after it completes. Pretty elegant, honestly.

Step 2: Name Individual Elements for Custom Animations

By default, the entire page gets a single cross-fade. That's fine for simple cases, but to animate specific elements individually — like morphing a thumbnail into a hero image — you'll want to assign a view-transition-name:

/* List page */
.product-thumbnail {
  view-transition-name: product-hero;
}

/* Detail page */
.product-hero-image {
  view-transition-name: product-hero;
}

The browser will now interpolate the position, size, and opacity of the element across the transition, producing a smooth morph effect without a single line of JavaScript animation code. No GSAP, no Framer Motion — just CSS.

Step 3: Customize the Animation

/* Slide the old content out to the left */
::view-transition-old(root) {
  animation: 250ms ease-in both slide-out-left;
}

/* Slide the new content in from the right */
::view-transition-new(root) {
  animation: 250ms ease-out both slide-in-right;
}

@keyframes slide-out-left {
  to { transform: translateX(-100%); }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
}

Notice we're using transform instead of animating left or top. This is critical — transform animations run on the compositor thread and never trigger layout recalculations. If you take one thing away from this article, let it be this: always animate with transform and opacity.

Cross-Document Transitions (MPAs)

This is where things get really exciting. Cross-document view transitions work across full page navigations with zero JavaScript — just CSS.

Step 1: Opt In on Both Pages

Add this CSS rule to every page that should participate in transitions:

@view-transition {
  navigation: auto;
}

That's it. Both the source and destination pages need this rule, and they must share the same origin.

Step 2: Name Persistent Elements

/* Shared across pages */
.site-header {
  view-transition-name: main-header;
}

.page-title {
  view-transition-name: page-title;
}

When the user navigates, the browser automatically morphs elements with matching view-transition-name values between the old and new pages. It feels almost magical the first time you see it working.

Step 3: Customize with JavaScript Events (Optional)

For more advanced use cases, the API provides two events — pageswap (fires on the old page before snapshots are taken) and pagereveal (fires on the new page before first render):

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation.from?.url;

  // Apply a directional transition type
  if (from?.includes("/products")) {
    event.viewTransition.types.add("slide-from-right");
  }
});

/* Then in CSS */
html:active-view-transition-type(slide-from-right)
  ::view-transition-old(root) {
  animation-name: slide-out-left;
}

html:active-view-transition-type(slide-from-right)
  ::view-transition-new(root) {
  animation-name: slide-in-right;
}

The :active-view-transition-type() selector lets you apply different animations depending on the type of navigation — forward, back, product-to-detail, whatever your app needs.

Pairing View Transitions with Speculation Rules for Instant Navigation

Cross-document view transitions work best when the destination page is already loaded. If the navigation takes more than about four seconds, Chrome will skip the transition entirely. That's where the Speculation Rules API becomes an essential companion.

By prerendering likely navigation targets, the destination page is ready before the user even clicks — meaning the view transition animates instantly with no loading delay:

<script type="speculationrules">
{
  "prerender": [
    {
      "where": {
        "and": [
          { "href_matches": "/products/*" },
          { "not": { "selector_matches": ".external-link" } }
        ]
      },
      "eagerness": "moderate"
    }
  ]
}
</script>

The combination of speculation rules (for near-zero navigation latency) and view transitions (for smooth visual continuity) delivers native-app-quality UX on the open web. No framework required. I've used this combo on a couple of production sites now, and the difference users notice is dramatic.

Performance Impact on Core Web Vitals

View transitions aren't free. Understanding their performance profile helps you use them effectively without tanking your metrics.

LCP (Largest Contentful Paint)

Real-user monitoring data shows that cross-document view transitions add approximately 70 ms to LCP on repeat mobile pageviews. The impact scales with device CPU speed — slower devices see a bigger hit.

For pages where LCP is already flirting with the 2.5-second threshold, this matters.

Mitigation: Combine with speculation rules to prerender pages. The prerendered page's LCP element is already painted, so the transition overhead gets absorbed by time the user would otherwise spend waiting for network responses.

INP (Interaction to Next Paint)

View transitions can increase the presentational delay component of INP — the time between the browser processing event handlers and actually painting the next frame. The transition snapshotting and animation compositing add rendering work that the main thread has to coordinate.

Mitigation: Keep transition durations short (200–300 ms). Avoid complex multi-element transitions on interaction-heavy pages. Stick to transform and opacity exclusively — these animate on the compositor thread without triggering layout or paint.

CLS (Cumulative Layout Shift)

View transitions themselves don't cause CLS — as long as you animate compositor-friendly properties. However, if you animate top, left, width, or height, the browser must recalculate layout during the transition, which can register as a layout shift.

Mitigation: Stick to transform and opacity for all custom animations. The default cross-fade is compositor-safe by design.

Accessibility: Respecting Reduced Motion

Some users have vestibular disorders that make motion animations disorienting or physically uncomfortable. This isn't optional — always honor the prefers-reduced-motion setting:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0.001s !important;
  }
}

Setting a near-zero duration (rather than 0) preserves functionality for any code that listens to the animationend event. A subtle detail, but it prevents weird edge-case bugs.

For JavaScript-triggered transitions, you can check the preference programmatically:

const prefersReducedMotion =
  window.matchMedia("(prefers-reduced-motion: reduce)").matches;

if (!prefersReducedMotion) {
  document.startViewTransition(() => updateDOM());
} else {
  updateDOM();
}

Real-World Implementation Patterns

E-Commerce Product List to Detail

This is the classic use case — a grid of products where clicking one morphs the thumbnail into a full-size hero image on the detail page:

/* Product card on list page */
.product-card img {
  view-transition-name: var(--product-id);
  contain: layout;
}

/* Hero image on detail page */
.product-detail .hero {
  view-transition-name: var(--product-id);
  contain: layout;
}

Use CSS custom properties (or inline styles) to generate unique view-transition-name values per product. The contain: layout declaration prevents the element from affecting layout outside its bounds during the transition.

Tab or Accordion Switching

function switchTab(tabId) {
  if (!document.startViewTransition) {
    showTab(tabId);
    return;
  }

  document.startViewTransition({
    update: () => showTab(tabId),
    types: ["tab-switch"],
  });
}

/* CSS */
html:active-view-transition-type(tab-switch)
  ::view-transition-old(tab-content) {
  animation: 200ms ease-out fade-and-scale-down;
}

html:active-view-transition-type(tab-switch)
  ::view-transition-new(tab-content) {
  animation: 200ms ease-in fade-and-scale-up;
}

View transition types let you apply different animations for different interaction patterns within the same page. Tabs get a quick scale-and-fade, navigations get a slide — each interaction gets the motion that feels right for it.

Back/Forward Navigation Direction

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) return;

  const navType = navigation.activation.navigationType;
  if (navType === "traverse") {
    event.viewTransition.types.add("back-navigation");
  }
});

/* Slide right for back navigation */
html:active-view-transition-type(back-navigation)
  ::view-transition-old(root) {
  animation: 250ms ease-in slide-out-right;
}

html:active-view-transition-type(back-navigation)
  ::view-transition-new(root) {
  animation: 250ms ease-out slide-in-left;
}

Performance Checklist

Here's a quick checklist to get the benefits of view transitions without regressing your Core Web Vitals:

  • Use transform and opacity only. These animate on the compositor thread and don't trigger layout or paint.
  • Keep durations under 300 ms. Longer transitions block interaction and inflate INP.
  • Add contain: layout to elements with view-transition-name to prevent layout side effects.
  • Pair cross-document transitions with speculation rules so the destination page is prerendered and the transition fires instantly.
  • Test on low-end devices. The ~70 ms LCP overhead on mobile is noticeably higher on budget Android phones.
  • Honor prefers-reduced-motion. Always reduce or skip animations for users who request it.
  • Feature-detect before calling the API. Check that document.startViewTransition exists for SPA transitions. Cross-document transitions degrade automatically.
  • Limit named elements. Every view-transition-name creates additional snapshot layers. Only name the elements that truly benefit from individual animation.

Debugging View Transitions in DevTools

Chrome DevTools has dedicated tooling for inspecting view transitions, and it's genuinely useful:

  1. Open DevTools > Animations panel. View transitions show up as a dedicated animation group.
  2. Use the slow-motion controls (25%, 10%) to step through the transition frame by frame.
  3. In the Elements panel, look for the ::view-transition pseudo-element tree during an active transition. You can inspect and live-edit snapshot styles right there.
  4. Enable "Emulate CSS prefers-reduced-motion" in the Rendering tab to verify your accessibility fallback works correctly.

Frequently Asked Questions

Do view transitions work with React, Next.js, or other frameworks?

Yes. React 19 includes a <ViewTransition> component that integrates with the API natively. Next.js supports cross-document view transitions in its App Router. Astro has built-in support via its <ViewTransitions /> component. For other frameworks, you can call document.startViewTransition() directly in your router's navigation handler.

Do view transitions hurt SEO or Core Web Vitals scores?

When implemented correctly, the impact is minimal. The ~70 ms LCP overhead on mobile is real but small. Pairing transitions with speculation rules and prerendering largely neutralizes the cost. Use compositor-friendly properties (transform, opacity) and keep durations short to avoid INP regressions. The API doesn't affect crawlability or indexing.

Can I use view transitions on cross-origin pages?

No. Cross-document view transitions require both pages to share the same origin. This is a security restriction — the browser can't safely snapshot content from a different origin. Cross-origin transitions are being explored for future spec revisions, but there's no timeline for this yet.

What happens if the destination page takes too long to load?

Chrome skips the view transition if the navigation exceeds roughly four seconds. The page still loads normally — you just don't get the animated transition. This is yet another reason to pair cross-document transitions with speculation rules or at minimum <link rel="prefetch"> for critical navigation paths.

How do view transitions differ from CSS @starting-style or Popover API animations?

@starting-style animates an element entering the DOM (like a popover appearing). View transitions animate between two complete states of a page or section — capturing before-and-after snapshots and interpolating between them. They solve different problems and can absolutely be used together. For instance, use @starting-style to animate a modal opening and view transitions for page-level navigation.

About the Author Editorial Team

Our team of expert writers and editors.