View Transitions API — Native-App-Smooth Page Transitions Without a Framework

Posted on: 4/20/2026 11:10:16 PM

89%Global browser coverage (Chrome, Edge, Firefox, Safari)
~70msLCP increase on mobile when enabled
0 JSCross-document transitions need no JavaScript
2-3xPerceived speedup on low-end devices

Building smooth page transitions on the web has always been hard. SPA frameworks like Vue Router or React Router mostly solved it by keeping the shell and swapping the content, but with a traditional MPA (Multi-Page Application) every navigation meant a blank screen and a full reload. The View Transitions API completely changes that: browsers natively support smooth transitions between two DOM states, works for both SPA and MPA, and doesn't depend on any framework.

1. Why do we need View Transitions?

1.1 The problem with traditional navigation

When a user clicks a link on an MPA, the browser goes through a sequential process:

sequenceDiagram
    participant U as User
    participant B as Browser
    participant S as Server
    U->>B: Click link
    B->>B: Unload old page (blank screen)
    B->>S: HTTP Request
    S-->>B: HTML Response
    B->>B: Parse HTML + CSS
    B->>B: Layout + Paint
    B->>U: Show new page
    Note over U,B: 200-800ms of blank screen
feels "janky"

Traditional navigation — the blank gap between pages produces a broken experience

That blank gap is why users feel web apps are "slower" than native apps, even when actual load time is very fast. The issue isn't absolute speed — it's perceived performance.

1.2 Older solutions and their limits

SolutionProsCons
SPA framework (Vue/React Router)Smooth transitions, keeps shellLarge bundle, SEO complexity, slow initial load
Manual CSS animationLightweight, full controlComplex state management, doesn't work cross-document
FLIP AnimationGreat performance (compositor-only)Complex code, manual position calculations
Barba.js / SwupEasy MPA transitionsExtra dependency, accessibility issues, hard to maintain

View Transitions API solves it cleanly

Rather than orchestrating transitions with JavaScript, the View Transitions API lets the browser snapshot the old state, perform the DOM update, and animate from the snapshot to the new state. The entire animation runs on the compositor thread — no main-thread blocking, no jank.

2. Architecture and Lifecycle

2.1 Same-Document Transitions (SPA)

The basic form — both states live in the same document:

graph LR
    A["startViewTransition()"] --> B["Capture Phase
Snapshot old DOM"] B --> C["Update Phase
Callback applies DOM changes"] C --> D["Animation Phase
Animate old -> new"] D --> E["Cleanup
Remove snapshots"] style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style C fill:#2c3e50,stroke:#fff,color:#fff style D fill:#e94560,stroke:#fff,color:#fff style E fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50

Same-document view-transition lifecycle

Minimal code to trigger a transition:

// Wrap DOM updates inside startViewTransition
document.startViewTransition(() => {
  // Any DOM changes happen here
  document.getElementById('content').innerHTML = newContent;
});

Just one wrapper line — the browser handles the rest: snapshot, animation, cleanup.

2.2 Cross-Document Transitions (MPA)

This is the real breakthrough. With an MPA, two entirely different pages (different HTML documents) can transition smoothly — and it needs zero JavaScript:

/* Add to the CSS of BOTH pages */
@view-transition {
  navigation: auto;
}

When the browser sees both the outgoing and incoming pages opting in via @view-transition, it automatically:

  1. Snapshots the old page before unload
  2. Loads the new page
  3. Cross-fades between the snapshot and the new page

Important constraint

Cross-document transitions only work between two pages of the same origin (same-origin navigation). Cross-domain navigation will not trigger transitions. This is a security constraint — the browser cannot snapshot cross-origin content.

2.3 The pseudo-element tree

During a transition, the browser builds a pseudo-element tree overlaid on top of the document. Understanding this tree is the key to custom animations:

::view-transition — root overlay, covers the viewport
  └─ ::view-transition-group(*) — container for each named element
      └─ ::view-transition-image-pair(*) — holds the old/new pair
          ├─ ::view-transition-old(*) — frozen snapshot (static image of old page)
          └─ ::view-transition-new(*) — live representation (the actual new page)

Each element with a view-transition-name creates its own group. Elements without a name share the default root group.

3. Custom Animations — in practice

3.1 Named Transitions — animate individual elements

Instead of cross-fading the whole page, you can name specific elements so the browser animates them independently:

/* Source page (list page) */
.article-card {
  view-transition-name: hero-article;
}

/* Destination page (detail page) */
.article-header-image {
  view-transition-name: hero-article;
}

The browser computes positions/sizes and animates .article-card.article-header-image — an "expand" effect just like native apps. Same view-transition-name across two pages = the browser treats them as "the same element" to morph.

3.2 Custom keyframes

The default is a cross-fade, but you can completely override it:

/* Slide the old page left */
::view-transition-old(root) {
  animation: slide-out 0.3s ease-in forwards;
}

/* Slide the new page in from the right */
::view-transition-new(root) {
  animation: slide-in 0.3s ease-out forwards;
}

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

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

3.3 Transition Types — different animation per direction

In SPAs, you can assign a type to each transition to pick the right animation:

// Navigate forward -> slide right
document.startViewTransition({
  update: () => navigateToDetail(id),
  types: ['slide-forward']
});

// Navigate back -> slide left
document.startViewTransition({
  update: () => navigateToList(),
  types: ['slide-back']
});
/* CSS targets each type */
:active-view-transition-type(slide-forward) {
  &::view-transition-old(root) {
    animation: slide-out-left 0.3s ease-in;
  }
  &::view-transition-new(root) {
    animation: slide-in-right 0.3s ease-out;
  }
}

:active-view-transition-type(slide-back) {
  &::view-transition-old(root) {
    animation: slide-out-right 0.3s ease-in;
  }
  &::view-transition-new(root) {
    animation: slide-in-left 0.3s ease-out;
  }
}

3.4 Cross-document events — pageswap and pagereveal

For MPAs, two new events let you intervene in the transition between documents:

sequenceDiagram
    participant Old as Old page
    participant B as Browser
    participant New as New page

    Old->>B: Navigation triggered
    B->>Old: Dispatch "pageswap" event
    Note over Old: event.viewTransition
can skip() or
customize snapshots B->>B: Snapshot old page B->>B: Load new page B->>New: Dispatch "pagereveal" event Note over New: event.viewTransition
can customize
animation from incoming page B->>B: Animate old -> new B->>New: Transition complete

Cross-document transition lifecycle with pageswap/pagereveal

// On the destination page — customize based on incoming URL
window.addEventListener('pagereveal', (event) => {
  if (!event.viewTransition) return;

  const url = new URL(window.location);

  // Use hero animation if we're navigating into a detail page
  if (url.pathname.match(/\/article\/\d+/)) {
    event.viewTransition.types.add('hero-expand');
  }
});

4. Impact on Core Web Vitals

The View Transitions API is not a "free lunch" — there is a measurable cost on Core Web Vitals.

4.1 Real data from RUM (Real User Monitoring)

An A/B test across 5 websites and 500,000+ pageviews over 7 days reported:

ConditionLCP increase (mobile)LCP increase (desktop)
No throttling + warm network cache~70ms~5ms
6x CPU slowdown4-29ms-
20x CPU slowdown (extreme)40-77ms-

What the 70ms number means

Adding 70ms to LCP on repeat mobile pageviews sounds small, but Google recommends LCP under 2.5 seconds. If your site is at 2.4s, +70ms can push you over the line. On the other hand, if LCP is at 1.5s, 70ms is well within budget — and you get significantly better perceived performance in exchange.

4.2 Why does LCP increase?

graph TD
    A["Navigation starts"] --> B["Browser snapshots old page"]
    B --> C["Load new page HTML/CSS"]
    C --> D["Parse + Render new page"]
    D --> E{"View Transition
enabled?"} E -->|No| F["Paint immediately -> LCP recorded"] E -->|Yes| G["Wait for snapshot ready"] G --> H["Run cross-fade animation"] H --> I["Paint final frame -> LCP recorded"] style E fill:#ff9800,stroke:#fff,color:#fff style G fill:#e94560,stroke:#fff,color:#fff style F fill:#4CAF50,stroke:#fff,color:#fff style I fill:#4CAF50,stroke:#fff,color:#fff

View Transition adds snapshot + animate steps before LCP is recorded

Essentially, the browser must wait for the snapshot and run the first animation frame before painting — so LCP is delayed by exactly the snapshot + first-frame time.

4.3 CLS and INP are not affected

Good news: View Transitions do not affect CLS (Cumulative Layout Shift) because animations run on the overlay pseudo-elements, not on the actual document. INP (Interaction to Next Paint) is also unaffected because transitions run on the compositor thread and don't block the main thread.

5. Optimization strategies — eliminating the overhead

5.1 Combine with Speculation Rules API

This is the strongest strategy: use the Speculation Rules API to prerender the destination page before the user clicks. When the transition fires, both pages are already rendered — the animation plays between two fully-rendered states with zero LCP penalty.

<script type="speculationrules">
{
  "prerender": [
    {
      "where": {
        "href_matches": "/*"
      },
      "eagerness": "moderate"
    }
  ]
}
</script>

<style>
@view-transition {
  navigation: auto;
}
</style>

Why this combo works

Speculation Rules prerenders the destination page → when the user clicks, the new page is already in memory → View Transitions only need to animate between two rendered states → no parse/render delay → LCP is unaffected. This is the pattern the Chrome team officially recommends.

5.2 Respect prefers-reduced-motion

Users can turn off animations via OS settings. Always wrap View Transitions in a media query:

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.3s;
  }
}

This way, users with reduced-motion enabled → no transition → no LCP penalty → both accessibility and performance are covered.

5.3 Only enable on larger screens

If mobile LCP is already near the 2.5s line, consider enabling View Transitions on desktop only (where the overhead is ~5ms):

@media (prefers-reduced-motion: no-preference) and (min-width: 768px) {
  @view-transition {
    navigation: auto;
  }
}

5.4 Keep animations short

Longer animations = longer LCP delay. Recommendations:

Transition typeSuggested durationWhy
Cross-fade (default)150-250msNoticeable but not a meaningful delay
Slide/Morph200-350msNeeds more time for spatial movement
Hero expand300-500msElements traveling long distances need more time

6.1 Vue Router

Vue Router doesn't integrate View Transitions natively, but you can combine them via a router guard:

// router/index.ts
router.beforeResolve((to, from) => {
  // Only transition on actual navigation (not initial load)
  if (from.name === undefined) return;

  return new Promise((resolve) => {
    document.startViewTransition(() => {
      resolve();
      // Vue Router will update DOM after resolve
    });
  });
});
<!-- Component to animate -->
<template>
  <div class="product-card" :style="{ viewTransitionName: `product-${id}` }">
    <img :src="image" />
    <h3>{{ name }}</h3>
  </div>
</template>

6.2 Nuxt

Nuxt 3 supports View Transitions via config:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    viewTransition: true // Enable cross-document transitions
  }
});

6.3 Astro

Astro was the first framework to integrate View Transitions natively with its own directive:

---
// layout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <img transition:name="hero" src={heroImage} />
    <slot />
  </body>
</html>

7. Practical production patterns

7.1 Progressive enhancement

Always check browser support before using the API:

// Feature detection
function navigateWithTransition(url, updateFn) {
  if (!document.startViewTransition) {
    // Fallback: navigate normally
    updateFn();
    return;
  }

  document.startViewTransition(() => updateFn());
}

7.2 Avoid view-transition-name conflicts

The most common mistake

Each view-transition-name must be unique in the document at the time of the transition. Two elements sharing a name will cause the transition to be skipped. Typical mistake: assigning the same name to every item in a list → only the last item animates.

/* WRONG — duplicate name */
.card { view-transition-name: card; }

/* CORRECT — unique name per id */
.card-1 { view-transition-name: card-1; }
.card-2 { view-transition-name: card-2; }

/* Or use inline style (Vue/React) */
/* :style="{ viewTransitionName: `card-${item.id}` }" */

7.3 Combine with skeleton loading

For pages with heavy content, combine skeletons + View Transitions for a smooth experience:

// Transition into the skeleton first
document.startViewTransition(async () => {
  showSkeleton();
});

// When data is ready, transition from skeleton -> real content
const data = await fetchData();
document.startViewTransition(() => {
  renderContent(data);
});

8. Browser support and timeline

Chrome 111 (March 2023)
Same-document View Transitions (SPA) — the first API shipped in a stable browser.
Chrome 126 (June 2024)
Cross-document View Transitions (MPA) — the breakthrough that enables MPA transitions without JS.
Safari 18 (September 2024)
Apple begins supporting same-document transitions in Safari and iOS WebKit.
Firefox 144 (January 2026)
Firefox completes same-document transition support — reaches Baseline Newly Available.
April 2026 (today)
Global coverage hits ~89%. Cross-document transitions work on Chrome, Edge, Safari. Firefox is rolling them out.

9. Conclusion

The View Transitions API is a big step toward closing the gap between web apps and native apps. Its core strength is being a web standard — no dependencies, no bundle size, no framework lock-in. For MPAs, just two lines of CSS are enough for smooth transitions.

That said, the LCP impact must be considered, especially on mobile. The optimal strategy combines Speculation Rules API to prerender + prefers-reduced-motion to respect accessibility. This combo gives you both perceived performance and clean Core Web Vitals.

Where to start?

If your site is an MPA with mobile LCP under 2.0s: add @view-transition { navigation: auto; } to your global CSS, wrap it in a prefers-reduced-motion media query, and add Speculation Rules for internal links. Three steps, zero JavaScript, significantly upgraded UX.

10. References