Everything you need to know about the View Transitions API in 2026 — from simple same-document animations to cross-document page transitions and the latest scoped view transitions. Practical code examples from a senior frontend developer.
For decades, smooth page transitions were the domain of JavaScript libraries and frameworks. React Router animations, Vue transition components, GSAP — all workarounds for a missing browser primitive. The CSS View Transitions API changes that fundamentally.
As of May 2026, the View Transitions API is production-ready across all major browsers. Same-document transitions (for SPAs) work in Chrome, Safari, and Firefox. Cross-document view transitions — animating between separate HTML pages during navigation — are now shippable in both Chromium and Safari, with Firefox following. The scoped view transitions announced at Google I/O 2026 bring scoped, element-level transitions for Canvas and SPA use cases.
In this guide, I cover every aspect of the View Transitions API: the core
startViewTransition() method, cross-document navigation with the
@view-transition CSS rule, custom animations with view-transition-name,
the new scoped transitions, browser support matrices, and real-world patterns you can use today.
I've been building with this API since Chrome 111 — these are battle-tested techniques.
If you're new to this API, think of it as the browser's native way to say: "capture the current page, change the DOM, then animate from the old state to the new one" — all declaratively, with zero JavaScript for basic cases.
The View Transitions API is a browser-native mechanism for animating changes between two DOM states. It works in three distinct modes:
document.startViewTransition(callback)
to animate a DOM update within a single page.@view-transition CSS at-rule or <meta> tag triggers
an automatic transition.
Under the hood, the browser captures a screenshot of the current state,
lets you update the DOM, then captures the new state, and interpolates
between the two using CSS animations. The browser handles all the pixel-level work —
no requestAnimationFrame, no ResizeObserver, no manual interpolation logic.
The simplest possible view transition — swapping content in a div:
async function switchView() { // Capture current state, update DOM, animate to new state const transition = await document.startViewTransition(async () => { // Your DOM mutation goes here contentDiv.innerHTML = newContent; }); // Wait for the transition animation to complete await transition.finished; }
That's it. The browser captures the before state (your current DOM), runs your callback (which modifies the DOM), captures the after state, and then runs a default crossfade animation between the two snapshots.
💡 Important: The callback passed to startViewTransition() should be
an async function. The browser captures the current state before the callback
runs and captures the new state after the promise resolves. If you use a synchronous
callback, the new state is captured immediately — which works for simple mutations but may miss
async rendering work.
Every view transition goes through a predictable lifecycle with several events you can hook into:
transition.ready — resolves when the pseudo-element tree is created and animations
are about to start. Use this to customize animation timing.transition.finished — resolves when the transition animations complete.transition.updateCallbackDone — resolves when your DOM update callback finishes
(before the animation starts).transition.skipTransition() — immediately moves to the new state without animating.
Useful for accessibility — honor prefers-reduced-motion.const transition = document.startViewTransition(async () => { updateDOM(); }); // Customize before animation starts transition.ready.then(() => { // Set custom animation durations document.documentElement.style.setProperty('--vt-duration', '800ms'); }); // Handle reduced motion if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { transition.skipTransition(); } await transition.finished;
This is the feature that makes the View Transitions API truly transformative. With cross-document view transitions, navigating between two separate HTML pages — an actual multi-page application (MPA) — produces a seamless animated transition. The browser captures the outgoing page, navigates to the new URL, renders the new page, and animates between the two.
Add the @view-transition CSS at-rule to both the source and destination pages:
/* On both the outgoing and incoming page */ @view-transition { navigation: auto; }
Alternatively, use a <meta> tag in the <head>:
<meta name="view-transition" content="same-origin">
Once enabled, all same-origin navigations between participating pages will automatically animate with a crossfade transition. Both pages must opt in — if only one has the rule, the transition is skipped.
⚠️ Important security note: Cross-document view transitions only work for same-origin navigations. Both pages must share the same protocol, domain, and port. Cross-origin navigations (e.g., linking to an external site) skip the transition entirely — the browser falls back to normal navigation.
The real magic happens with shared elements. When an element exists on both
the outgoing and incoming page with the same view-transition-name, the browser
animates that element smoothly between its old and new positions, while the rest of the page
crossfades. This is how you achieve the "material design" shared-axis transitions natively:
/* On page A — a product card image */ .product-image-main { view-transition-name: product-image; } /* On page B — the same image in a hero position */ .product-detail-image { view-transition-name: product-image; }
When navigating from page A (product listing) to page B (product detail), the product image smoothly scales and repositions from its card position to its hero position. The rest of the page content crossfades. No JavaScript needed for the transition itself.
💡 Best practice: Each view-transition-name must be unique within
a page. If two elements share the same name on the same page, the transition is skipped.
Use unique names like product-card-1, product-card-2 in listing views
and map them dynamically.
The default crossfade is clean but generic. You can customize every aspect of the animation
using CSS pseudo-elements and @keyframes.
During a view transition, the browser constructs a pseudo-element tree that represents the animation layers. The tree structure is:
::view-transition — the root overlay (covers the entire page)::view-transition-group(name) — a group for each named element (and the root
group for unnamed content)::view-transition-image-pair(name) — holds the old and new snapshot::view-transition-old(name) — the outgoing state snapshot::view-transition-new(name) — the incoming state snapshot
The browser renders the old and new snapshots into separate layers, places them in
::view-transition-old and ::view-transition-new, and runs default
CSS animations:
A slide-and-fade transition for the entire page:
/* Override the default animation for all groups */ ::view-transition-old(root) { animation-duration: 400ms; animation-name: slide-out; } ::view-transition-new(root) { animation-duration: 400ms; animation-name: slide-in; } @keyframes slide-out { 0% { opacity: 1; transform: translateX(0); } 100% { opacity: 0; transform: translateX(-30px); } } @keyframes slide-in { 0% { opacity: 0; transform: translateX(30px); } 100% { opacity: 1; transform: translateX(0); } }
Use named groups for element-specific animations. For example, a hero image that scales up while the rest of the page slides:
::view-transition-old(product-image) { animation-name: scale-down; } ::view-transition-new(product-image) { animation-name: scale-up; } @keyframes scale-up { 0% { transform: scale(0.8); opacity: 0.5; } 100% { transform: scale(1); opacity: 1; } } @keyframes scale-down { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(1.2); opacity: 0; } }
Always provide a prefers-reduced-motion fallback. Disable view transitions entirely
or use shorter, subtler animations:
@media (prefers-reduced-motion: reduce) { ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.01ms !important; } }
Announced at Google I/O 2026 in the "What's new in Web UI" session, scoped view transitions let you constrain a transition to a specific DOM element rather than the entire document. This is a critical improvement for:
Pass a scope option to startViewTransition():
const contentArea = document.getElementById('main-content'); await document.startViewTransition({ update: async () => { // Update only the main content area contentArea.innerHTML = newContent; }, scope: contentArea }).finished;
With scoped transitions, the old snapshot shows only the scope element's subtree,
and the new snapshot shows the updated subtree. The rest of the page remains static — no
unnecessary crossfade on the navigation bar or footer.
💡 Pro tip: Scoped view transitions pair perfectly with the HTML-in-Canvas API. When you update a DOM element that's rendered onto a Canvas, scope the transition to that element for smooth Canvas-side animations without full-page re-renders.
New in Chrome 150+ (July 2026): Scoped transitions now work across documents
for same-origin navigations. If both pages have a matching element with the same
view-transition-name, only that element's transition is animated — the rest
of the page crossfades normally. This is the ideal pattern for MPA sites with shared layouts
(header, footer, sidebar) and a dynamic content area:
/* On both pages */ #content-area { view-transition-name: page-content; } /* Header and footer have no view-transition-name — they crossfade */
The View Transitions API has come a long way. Here's the exact status as of May 2026:
| Feature | Chrome | Edge | Safari | Firefox |
|---|---|---|---|---|
| Same-document (startViewTransition) | 111+ | 111+ | 18+ | 121+ |
| Cross-document (meta/@view-transition) | 126+ | 126+ | 18+ | 131+ (partial) |
| Scoped transitions (same-document) | 148+ | 148+ | — | — |
| Cross-document scoped | 150+ | 150+ | — | — |
| Custom animations (::view-transition-old/new) | 111+ | 111+ | 18+ | 121+ |
| view-transition-name on multiple elements | 111+ | 111+ | 18+ | 121+ |
Key takeaway: Same-document transitions and cross-document transitions with basic crossfade are production-ready in all major browsers. Scoped transitions are Chrome-only in May 2026, but are expected to reach other browsers by late 2026 / early 2027 based on the WICG discussion momentum.
For React, Vue, or Svelte applications, wrap your router's navigation handler with
startViewTransition(). The framework still renders the new component — the
View Transitions API just animates the swap:
// React Router integration import { useNavigate } from 'react-router-dom'; function useViewTransitionNavigate() { const navigate = useNavigate(); return async (to, options) => { if (document.startViewTransition) { await document.startViewTransition(async () => { navigate(to, options); }).finished; } else { navigate(to, options); } }; }
When adding or removing items from a list, use view-transition-name on each item
for smooth positional animations:
function addItem(items, newItem) { const list = document.getElementById('item-list'); // Assign unique view-transition-name to each item list.querySelectorAll('.item').forEach((item, i) => { item.style.viewTransitionName = `item-${i}`; }); document.startViewTransition(async () => { list.appendChild(createItemElement(newItem)); // Reassign names after mutation list.querySelectorAll('.item').forEach((item, i) => { item.style.viewTransitionName = `item-${i}`; }); }); }
The classic use case — a thumbnail that expands to a full-size hero image with a morphing
animation. Both the thumbnail and the hero share the same view-transition-name:
/* Thumbnail in gallery */ .gallery-thumb[data-id="photo-1"] { view-transition-name: gallery-photo-1; } /* Hero image on detail page */ .photo-hero[data-id="photo-1"] { view-transition-name: gallery-photo-1; }
Navigating between the gallery and detail page produces a seamless morphing animation — the thumbnail scales up to fill the hero position while maintaining visual continuity.
Use scoped view transitions for tab content panels. Only the panel content fades/slides, while the tab bar and the rest of the page remain static:
async function switchTab(tabIndex) { const panel = document.getElementById(`tab-panel-${tabIndex}`); await document.startViewTransition({ update: async () => { panels.forEach(p => p.classList.remove('active')); panel.classList.add('active'); }, scope: panel.parentElement // Only the panel container animates }).finished; }
View Transitions are remarkably efficient because the browser handles all the compositing. However, there are important performance factors to consider:
startViewTransition(). Multiple sequential transitions cause flicker.The View Transitions API is a progressive enhancement. Your page should work perfectly without it — transitions just make it better. Here's a safe feature detection pattern:
if ('startViewTransition' in document) { // Modern browser — use view transitions await document.startViewTransition(async () => { updateDOM(); }).finished; } else { // Fallback — just update the DOM updateDOM(); }
For cross-document transitions, the @view-transition CSS at-rule is ignored
by browsers that don't support it, so it's safe to include unconditionally. The browser
simply performs a normal navigation without animation.
document.startViewTransition(callback) where callback updates the DOM. The API captures the current page state, runs your callback that changes the DOM, captures the new state, then animates between the two. Example: await document.startViewTransition(() => { updateUI(); }).finished; The callback should be async for best results.<meta name="view-transition" content="same-origin"> to both pages, and the browser automatically animates the transition when navigating between them. Both pages must be same-origin. Shared elements with matching view-transition-name animate smoothly between pages. For a complete comparison of SPA vs MPA architectures and how view transitions fit in, see my web application development guide.document.startViewTransition({ update: callback, scope: element }) to scope a transition.::view-transition-old (the outgoing snapshot) and ::view-transition-new (the incoming snapshot) combined with @keyframes animations. You can override animation-duration, animation-timing-function, and animation-name. Use view-transition-name for shared element transitions, which can crossfade independently from the rest of the page. For dynamic color transitions between themes, combine with CSS Relative Color Syntax to create smooth color animations driven by CSS custom properties.<meta> tag or use the CSS @view-transition rule, navigation transitions work without JavaScript. Same-document transitions (startViewTransition) require JavaScript because the DOM update itself needs scripting. For the best user experience, use cross-document transitions as a progressive enhancement and same-document transitions for SPA navigation.Here's the simplest possible implementation — a page that smoothly animates between light and dark mode:
<button id="themeToggle">Toggle Theme</button> <script> document.getElementById('themeToggle').addEventListener('click', async () => { await document.startViewTransition(async () => { document.body.classList.toggle('dark-mode'); }).finished; }); </script>
That's it. One method call wraps a DOM mutation and the browser handles the animation. The theme toggle gets a smooth crossfade — no extra CSS, no animation libraries.
For cross-document transitions, add to your pages:
<meta name="view-transition" content="same-origin">
And all same-origin navigations between pages with this meta tag will animate with a
smooth crossfade. Add view-transition-name to elements you want to morph
between pages — product images, hero sections, logos — and the browser handles the rest.
No JavaScript required for the animation itself.
The View Transitions API is one of the most impactful web platform additions in recent years. It makes polished, app-like transitions accessible to every website — from a simple blog to a complex SPA — with minimal code and maximum performance.
Need help implementing view transitions in your project? I'm available for frontend architecture consulting and development. View my services or contact me directly. For a broader look at how modern CSS features like container queries and view transitions work together, see my CSS Container Queries complete guide.
I build production web applications using the latest CSS and JavaScript APIs. Let's discuss your project — free consultation.