View Transitions API Building Native Feeling Web Apps
Learn how to use the View Transitions API to create seamless page and element animations in modern web apps. Covers same-document and cross-document transitions, React integration, shared element morphing, and accessibility best practices.

Introduction#
For most of the web’s history, navigation meant a hard cut. A user clicks a link, the current page disappears, and a new one appears. Native mobile apps have never worked this way. iOS and Android have always animated between screens with slides, fades, and element morphs that communicate spatial context and continuity. This visual gap between web and native has been one of the most persistent reasons users perceive web apps as feeling less polished.
The View Transitions API closes that gap. Now supported in all major browsers including Chrome 114+, Firefox 125+, Safari 17+, and Edge 114+, the API gives developers a browser-native mechanism to animate between DOM states and page navigations. No animation library required. No manual element position tracking. No mount and unmount lifecycle gymnastics.
This guide covers how the API works, practical implementation patterns for both single-page and multi-page applications, integration with React and Next.js, and the accessibility considerations every production implementation must address.
How the View Transitions API Works#
The View Transitions API operates by capturing a snapshot of the current DOM state, applying the DOM change, capturing the new state, and then animating between the two snapshots using CSS animations. The entire process is handled by the browser’s compositor thread, which means transitions run at 60fps or higher without blocking the main thread.
The key steps in a same-document (SPA) transition are:
Developer calls document.startViewTransition(callback)
The browser takes a screenshot of the current page
The callback function runs, updating the DOM
The browser takes a screenshot of the new state
The browser creates a pseudo-element tree and animates between the two states
Once the animation completes, the pseudo-elements are removed
The default animation is a cross-fade. What makes the API powerful is that you can customize this animation with CSS and, more importantly, designate individual elements as shared elements that morph between their old and new positions.
Basic Implementation#
For a simple page transition in an SPA, the implementation is minimal:
async function navigateTo(url) {
// Check browser support and fall back gracefully
if (!document.startViewTransition) {
await updatePage(url);
return;
}
const transition = document.startViewTransition(async () => {
await updatePage(url);
});
await transition.finished;
}
async function updatePage(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").replaceWith(doc.querySelector("main"));
}
This produces a smooth cross-fade between the old and new page content. The fallback for unsupported browsers is a straightforward page update with no animation.
Shared Element Transitions#
The most visually compelling use of the View Transitions API is shared element morphing animating a specific element from its position on the current page to its position on the next page. A classic example is a product card expanding into a product detail page.
To designate an element as a shared element, assign it a unique view-transition-name in CSS:
/* Product card on the listing page */
.product-card[data-id="42"] {
view-transition-name: product-42;
}
/* Product image on the detail page */
.product-detail-image {
view-transition-name: product-42;
}
When a view transition occurs and both the old and new DOM contain elements with the same view-transition-name, the browser automatically animates between them morphing the element’s size, position, and appearance from one state to the other.
For dynamic content where the view-transition-name cannot be hardcoded, set it in JavaScript just before triggering the transition:
async function openProduct(productId) {
// Assign the name to the card being clicked
const card = document.querySelector(`[data-product-id="${productId}"]`);
card.style.viewTransitionName = `product-${productId}`;
document.startViewTransition(async () => {
await loadProductDetail(productId);
// The name on the detail page must match
document.querySelector(".product-hero").style.viewTransitionName = `product-${productId}`;
});
}
Important Constraint#
Each view-transition-name value must be unique in the DOM at any given moment. If two elements share the same name simultaneously, the browser disables the transition for both and falls back to an instant swap. In list-based scenarios, ensure you only set the name on the element being interacted with, not on all list items at once.
Customizing Transitions with CSS#
The View Transitions API exposes a set of CSS pseudo-elements that you can target to customize animation behavior:
/* The root transition: controls the overall page crossfade */
::view-transition-old(root) {
animation: 200ms ease-out fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-out fade-in;
}
/* A shared element transition */
::view-transition-old(product-hero) {
animation: 300ms ease-in-out morph-out;
}
::view-transition-new(product-hero) {
animation: 300ms ease-in-out morph-in;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
You can also use the transition-type mechanism introduced in newer browser versions to apply different animations to forward and backward navigation:
document.startViewTransition({
update: updateCallback,
types: ["slide-forward"]
});
/* Apply directional slide based on navigation type */
html[data-astro-transition="slide-forward"]::view-transition-new(root) {
animation: slide-in-from-right 300ms ease-out;
}
html[data-astro-transition="slide-forward"]::view-transition-old(root) {
animation: slide-out-to-left 300ms ease-out;
}
Cross-Document Transitions in MPAs#
For multi-page applications where navigation involves full page loads, cross-document view transitions require no JavaScript at all. They are enabled entirely through CSS:
/* Enable cross-document transitions for the entire site */
@view-transition {
navigation: auto;
}
With this single CSS declaration, the browser automatically animates between any two pages on the same origin. Both pages need the declaration for the transition to apply in both directions.
For shared elements across page navigations, the same view-transition-name pattern applies. If a product image appears in a card on the listing page and as a hero on the detail page, naming both with the same value is all that is required for the browser to morph between them on navigation.
Cross-document view transitions are supported in Chrome and Edge from version 126+, with Firefox and Safari support following in 2025 and 2026 respectively. Always include @view-transition { navigation: auto; } only as progressive enhancement, since unsupported browsers simply navigate without animation.
React and Next.js Integration#
Next.js 15 introduced native ViewTransition component support that integrates with the App Router’s client-side navigation. Rather than manually calling document.startViewTransition, the framework wraps navigation events automatically.
// app/layout.tsx
import { ViewTransitions } from "next/view-transitions";
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<ViewTransitions />
</head>
<body>{children}</body>
</html>
);
}
// components/ProductCard.tsx
import { Link } from "next-view-transitions";
export function ProductCard({ product }) {
return (
<article
style={{ viewTransitionName: `product-${product.id}` }}
>
<img src={product.image} alt={product.name} />
<Link href={`/products/${product.id}`}>
{product.name}
</Link>
</article>
);
}
React 19 introduced a first-class useViewTransition hook and startTransition integration that allows state-driven DOM updates to participate in view transitions without manual API calls. This makes view transitions composable with React’s existing concurrent rendering model.
Performance Considerations#
View transitions are compositor-thread animations, which means they have a minimal performance footprint compared to JavaScript-driven animations. However, a few patterns can degrade performance if not handled carefully.
Animating elements with complex CSS properties like box-shadow, filter, or border-radius during a transition can cause the browser to repaint on every frame rather than delegating to the compositor. Where possible, limit shared element transitions to transform and opacity changes, which are the properties the compositor can animate entirely off the main thread.
For pages with large amounts of content, the snapshot-taking step can be expensive. If you notice frame drops at the start of transitions on content-heavy pages, reducing the scope of the transition by targeting specific named elements rather than the entire root can help significantly.
Transition duration matters. Animations longer than 300 to 400ms tend to feel sluggish and make the application feel slower rather than smoother. Aim for the shortest duration that still communicates spatial context clearly.
Accessibility#
Motion on screen is a meaningful accessibility concern. Some users experience vestibular disorders or motion sensitivity that make animated transitions physically uncomfortable. The View Transitions API does not automatically respect the user’s preference for reduced motion.
Always wrap transition animations in a prefers-reduced-motion media query and provide a minimal or instant fallback:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
/* For shared elements, reduce to opacity only */
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}
This ensures users who have opted into reduced motion in their operating system get an accessible experience while others get the full transition.
When to Use View Transitions#
View transitions deliver the most value in applications where spatial navigation matters to user comprehension. E-commerce flows where a product card expands into a detail view, dashboard navigation between related data surfaces, onboarding sequences with a clear directional flow, and document or content management interfaces where users need to understand where they came from and where they are going are all strong candidates.
View transitions add less value and may add unnecessary complexity in applications with flat navigation structures, simple utility interfaces, or any context where speed of navigation is more important than perceived continuity.
References#
MDN Web Docs: Using the View Transition API
Next.js Official Guide: View Transitions
Chrome for Developers: View Transitions API
Frontend Masters: The View Transitions API
DEV Community: Mastering Smooth Page Transitions with the View Transitions API
Last updated