Production engineering guide for view transitions api covering patterns, implementation strategies, and operational best practices.
View Transitions API enables declarative, performant, and coordinated animations between views in a web application. It is essential when navigating between pages, switching between states, or animating between different content layouts, ensuring that transitions feel seamless and responsive. The API is most valuable in SPAs, progressive web apps, and any client-side application where visual continuity between states is critical.
A view transition is a coordinated animation between two views: the *old view* (current) and the *new view* (target). The browser tracks visual elements across the transition and composites them using a single, high-performance rendering pass. To define a view transition, you declare view-transition-name CSS properties on elements and invoke startViewTransition() in JavaScript.
To initiate a view transition, call startViewTransition() with a callback that updates the DOM. The callback runs in a new frame, and the browser captures the visual state of the old view and applies it to a *transition container*.
document.getElementById('nav-link').addEventListener('click', async (event) => {
event.preventDefault();
const href = event.currentTarget.href;
const transition = document.startViewTransition(async () => {
// Update the DOM in the new view
const response = await fetch(href, { headers: { 'Accept': 'text/html' } });
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newContent = doc.querySelector('#main-content');
document.getElementById('main-content').replaceWith(newContent);
// Update the URL
history.pushState(null, '', href);
});
// Optional: add a background to the transition
transition.ready.then(() => {
document.body.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
});
});
The startViewTransition() method returns a ViewTransition object. The ready promise resolves when the transition is fully rendered and ready for user interaction.
view-transition-nameTo define which elements transition between views, assign view-transition-name CSS values to elements in both the old and new view. The browser matches elements with the same name across the transition.
.hero-image {
view-transition-name: hero;
}
.hero-text {
view-transition-name: hero-text;
}
.nav-link {
view-transition-name: nav-link;
}
When navigating, the browser identifies hero elements in both views and animates them as a single unit. The animation is computed using the *bounding box*, *position*, *size*, and *content* of the elements. The browser synthesizes a composite layer from the old view and overlays the new view, applying the transition animation.
The transition begins immediately after startViewTransition() is called. The browser captures the current DOM, composites the old view, and applies the new view on top. The animation is triggered by the view-transition CSS property, which controls how the transition is animated.
.view-transition {
view-transition-name: page;
view-transition-duration: 0.6s;
view-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Optional: define the transition behavior */
.view-transition::view-transition {
background: #f0f0f0;
backdrop-filter: blur(10px);
border-radius: 12px;
}
The ::view-transition pseudo-element represents the transition container. It is rendered once per transition and can be styled independently. This layer is used to composite the old and new views during the transition.
View transitions work best when combined with CSS animations and keyframes. The browser automatically animates all elements with the same view-transition-name, but you can augment this with @keyframes and animation rules.
.hero-image {
view-transition-name: hero;
width: 100%;
height: 400px;
object-fit: cover;
}
.hero-image::view-transition {
background: #000;
opacity: 0.8;
}
@keyframes slideIn {
from {
transform: translateX(100px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.hero-text {
view-transition-name: hero-text;
animation: slideIn 0.6s ease-out forwards;
}
The browser synchronizes the animation timing across the transition, ensuring that elements animate together even if the new view is loaded asynchronously.
When the new view is fetched asynchronously, the transition may appear to "pop" before the content arrives. To avoid this, use transition.ready to animate the background or overlay after the DOM update.
const transition = document.startViewTransition(async () => {
const response = await fetch('/blog/post-1');
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const newPost = doc.querySelector('#post');
document.getElementById('content').replaceWith(newPost);
});
transition.ready.then(() => {
document.body.classList.add('transition-complete');
});
This ensures that the background or overlay animation starts only after the DOM has settled, giving the illusion of instant navigation.
You can have multiple independent transitions in a single navigation. The browser composites all active transitions, allowing layered animations.
// Transition 1: hero image
document.querySelector('.hero-link').addEventListener('click', async (e) => {
e.preventDefault();
const transition1 = document.startViewTransition(async () => {
const response = await fetch(e.currentTarget.href);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const content = doc.querySelector('#hero-content');
document.getElementById('hero').replaceWith(content);
});
transition1.ready.then(() => {
document.body.style.backgroundImage = 'url(/hero-bg-2.jpg)';
});
});
// Transition 2: navigation bar
document.querySelector('.nav-link').addEventListener('click', async (e) => {
e.preventDefault();
const transition2 = document.startViewTransition(async () => {
const response = await fetch(e.currentTarget.href);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const nav = doc.querySelector('nav');
document.querySelector('header').replaceWith(nav);
});
transition2.ready.then(() => {
document.querySelector('header').style.transform = 'translateY(-10px)';
});
});
Each startViewTransition() call creates a separate transition container, and the browser composites them in the correct order.
View Transitions API is not universally supported. The browser may fall back to a simple transition or animate() API when startViewTransition() is unavailable. To ensure consistency, use feature detection.
if ('startViewTransition' in document) {
// Use View Transitions API
const transition = document.startViewTransition(async () => {
// Update DOM
});
} else {
// Fallback: use CSS transitions or keyframe animations
const element = document.getElementById('content');
element.style.transition = 'all 0.4s ease-out';
element.style.transform = 'translateX(-100px)';
element.style.opacity = 0;
const newContent = await fetch('/page').then(r => r.text());
element.innerHTML = newContent;
requestAnimationFrame(() => {
element.style.transform = 'translateX(0)';
element.style.opacity = 1;
});
}
view-transition-name values: If an element in the old view has view-transition-name: hero, but the new view has view-transition-name: "hero", the transition fails silently. The browser compares strings exactly, including whitespace. Use view-transition-name: hero in both views.view-transition styles: If ::view-transition is not defined, the transition container is invisible, and the background appears flat. Always define ::view-transition for visual polish.@keyframes are used, the browser starts the animation on the new view, but the old view is already visible. The animation may appear to “jump” or “slide in” after the transition starts. Use view-transition-duration and view-transition-timing-function on the ::view-transition element to align timing.transition.ready promise resolves after the transition is composited, but the background color change may not appear unless explicitly applied to the body or html element.view-transition-name on only the most important elements to reduce compositing cost.::view-transition styles for performance: avoid box-shadow, filter, and background-image on large elements.view-transition-duration on the ::view-transition pseudo-element to control the overall timing.view-transition-name is not set on both views, or when the startViewTransition() callback runs in a different frame than the current one.startViewTransition() callback, causing the browser to render the new view as a separate step.view-transition-name elements have different transform, scale, or padding values in the old and new view. Use transform: translate() and scale() consistently across both views.document.startViewTransition(callback): Returns a ViewTransition object.ViewTransition.ready: Promise resolving when transition is ready for user interaction.ViewTransition.updateCallback: Function that runs after the transition starts.ViewTransition.cancel(): Cancel the active transition.ViewTransition.ready.then(): Run code after the transition is fully rendered.The View Transitions API is a powerful tool for creating rich, responsive navigation experiences. It is not a drop-in replacement for CSS animations, but a coordinated, compositing-level mechanism that works best when combined with semantic naming and careful layout design.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy