View transitions
Baseline 2025 newly available
Supported in Chrome: yes.
Supported in Edge: yes.
Supported in Firefox: yes.
Supported in Safari: yes.
Since October 2025 this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.
The View Transitions API makes your web app feel native without animation libraries.
It creates smooth transitions between different DOM states. When a user clicks a thumbnail to see the full image, or filters a list, or navigates between pagesâthe browser handles the animation automatically.
Before this API, youâd reach for libraries like Framer Motion or React Spring to animate DOM changes. Those libraries work, but they add bundle size, require learning new APIs, and can be complex to integrate. The View Transitions API is built into the browser and works with any frameworkâor no framework at all.
The browser does the heavy lifting. You tell it when the DOM changes. It captures what the page looked like before, what it looks like after, and animates between those two states. No manual keyframe management, no calculating element positions, no performance optimization needed.
How it works
Call document.startViewTransition() and pass it a function that updates the DOM. The browser captures a snapshot of the old state, executes your function, captures the new state, and animates between them.
The API works in three phases. First, the browser takes a screenshot of the current page. Second, it calls your callback function to update the DOM. Third, it takes another screenshot and animates from the old screenshot to the new one. This happens in a single frame, so users never see the instant DOM changeâonly the smooth animation.
function updateView() { // Check browser support if (!document.startViewTransition) { updateTheDOMSomehow(); return; }
// Wrap DOM changes in a transition document.startViewTransition(() => updateTheDOMSomehow());}The default animation is a cross-fade. No CSS required. The browser figures out what changed and animates it.
Try it: Click the button below to see the basic transition in action.
Note: If your browser doesn't support View Transitions, the content will still toggle without animation.
The transition returns a promise that resolves when the animation completes. Use this to chain actions or trigger cleanup:
const transition = document.startViewTransition(() => { document.querySelector('#content').textContent = 'New content';});
// Wait for the animation to finishawait transition.ready;console.log('Transition started');
await transition.finished;console.log('Transition completed');If your callback throws an error or returns a rejected promise, the transition aborts. The DOM still updates, but without animation. This fail-safe behavior means transitions enhance the experience without breaking core functionality.
For multi-page apps, add this CSS to enable transitions between different pages:
@view-transition { navigation: auto;}Now when users click links, the browser animates between pages instead of doing a hard navigation. This works in Chrome 126+, Edge 126+, and Safari 18.2+.
You can customize which navigations trigger transitions. Use JavaScript to control this per-navigation:
navigation.addEventListener('navigate', (event) => { if (shouldNotTransition(event)) { return; // Skip transition }
event.intercept({ handler: async () => { document.startViewTransition(() => { // Load new content }); } });});Custom animations
Use CSS pseudo-elements to control how transitions look. The API creates several pseudo-elements during each transition:
::view-transition- the root overlay containing all transitions::view-transition-group()- groups related elements::view-transition-image-pair()- contains both old and new snapshots::view-transition-old()- snapshot of the previous state::view-transition-new()- live view of the new state
These pseudo-elements exist only during the transition. The browser creates them, animates them, then removes them. You can style them like any CSS elementâtransform, opacity, filter, whatever you need.
::view-transition-old(root) { animation: slide-out-left 0.3s ease-out;}
::view-transition-new(root) { animation: slide-in-right 0.3s ease-out;}
@keyframes slide-out-left { to { transform: translateX(-100%); }}
@keyframes slide-in-right { from { transform: translateX(100%); }}This creates a slide transition instead of the default fade.
Try it: Navigate through the steps to see the custom slide animation.
Custom slide animations defined with @keyframes and ::view-transition pseudo-elements.
For specific elements, use view-transition-name in CSS to create named transitions:
.thumbnail { view-transition-name: product-image;}
.fullscreen { view-transition-name: product-image;}When an element with view-transition-name: product-image disappears and another appears with the same name, the browser morphs between them. The thumbnail smoothly expands into the fullscreen view.
The browser tracks the elementâs position, size, and other properties, then animates from the old state to the new state. This works even when the elements are completely differentâone could be a 200px thumbnail and the other a full-screen modal. The browser figures out the transformation needed.
Try it: Click any image to see it smoothly morph into fullscreen.
Click any image to expand
The thumbnail smoothly morphs into the fullscreen view using view-transition-name.
Named transitions work because the browser matches elements by their view-transition-name. If an element with product-image exists before and after the transition, the browser knows theyâre related and creates a morph animation. If only one exists, it fades in or out. If multiple elements have the same name, the last one wins.
You can animate multiple elements independently by giving them unique names:
.card-1 { view-transition-name: card-1; }.card-2 { view-transition-name: card-2; }.card-3 { view-transition-name: card-3; }Each named element gets its own pseudo-element tree. You can style them separately:
::view-transition-old(card-1) { animation: fade-out 0.3s ease-out;}
::view-transition-new(card-1) { animation: fade-in 0.3s ease-in;}This also works for animating list reordering. Give each item a unique view-transition-name and the browser animates them to their new positions:
.list-item-1 { view-transition-name: item-1; }.list-item-2 { view-transition-name: item-2; }.list-item-3 { view-transition-name: item-3; }Try it: Click shuffle to see items animate to their new positions.
Each item has a unique view-transition-name so the browser animates them to their new positions.
The browser calculates the shortest path between old and new positions for each named element. Items that move animate smoothly to their new locations. Items that disappear fade out. Items that appear fade in. All coordinated automatically.
Dynamic transition names work with JavaScript. Generate names based on data:
items.forEach((item, index) => { const element = document.querySelector(`#item-${item.id}`); element.style.viewTransitionName = `item-${item.id}`;});
document.startViewTransition(() => { // Reorder items items.sort(() => Math.random() - 0.5); renderItems(items);});Each item keeps its identity across the transition. The browser tracks where each one moves and animates it there.
Performance considerations
View Transitions are fast. The browser captures bitmap snapshots of elements, which is cheaper than animating actual DOM nodes. Snapshots render on the GPU, so animations run at 60fps even on slower devices.
The snapshots are temporary. They exist only during the transitionâusually 200-400ms. After the animation completes, theyâre discarded. This means no memory leaks or lingering overhead.
Transitions donât block the main thread. The browser captures snapshots, starts the animation, then continues executing JavaScript. Your app stays responsive during transitions.
If a transition takes too long, the browser caps it. Transitions longer than 1 second are suspiciousâusually a sign of forgotten cleanup. The browser enforces reasonable limits to prevent stuck states.
Using with frameworks
React has experimental support via unstable_ViewTransition:
import { unstable_ViewTransition as ViewTransition, useState, startTransition } from 'react';
function Item() { return ( <ViewTransition> <div>Content here</div> </ViewTransition> );}
export default function Component() { const [showItem, setShowItem] = useState(false);
return ( <> <button onClick={() => { startTransition(() => { setShowItem(prev => !prev); }); }}> Toggle </button> {showItem ? <Item /> : null} </> );}The experimental React API requires installing React canary builds:
npm install react@experimental react-dom@experimentalDonât use this in production. The API is unstable and will change.
Vue, Svelte, and other frameworks work with the standard API. No special wrappers neededâjust call document.startViewTransition() when state changes. Most frameworks trigger DOM updates synchronously within the callback, which is exactly what the API expects.
For SPAs using client-side routing, wrap route changes in transitions:
// With React Routernavigate('/new-page');// becomesdocument.startViewTransition(() => { navigate('/new-page');});
// With Vue Routerrouter.push('/new-page');// becomesdocument.startViewTransition(() => { router.push('/new-page');});Astro has built-in support through <ViewTransitions />. Add the component to your layout and page transitions happen automatically. You can customize transition names per-page using the transition:name directive.
Common patterns
Conditional transitions based on user preference:
function updateWithTransition(updateFn) { if (prefersReducedMotion()) { updateFn(); return; }
document.startViewTransition(updateFn);}Skip transitions for fast repeated actions:
let lastTransition = 0;function updateIfReady(updateFn) { const now = Date.now(); if (now - lastTransition < 300) { updateFn(); // Too soon, skip transition return; }
lastTransition = now; document.startViewTransition(updateFn);}Different transitions for different actions:
document.documentElement.dataset.transition = 'slide';document.startViewTransition(() => { navigate('/next');});
// In CSS:root[data-transition='slide'] ::view-transition-old(root) { animation: slide-out 0.3s;}Browser support
Same-document transitions work in Chrome 111+, Edge 111+, Firefox 144+, and Safari 18+.
Cross-document transitions (the multi-page app feature) work in Chrome 126+, Edge 126+, and Safari 18.2+. Firefox doesnât support it yet.
The API degrades gracefully. If the browser doesnât support it, your DOM updates still workâthey just donât animate. This makes it safe to use today. Check for support with 'startViewTransition' in document before calling.
The API is stable. Chrome shipped it in 2023. Other browsers followed. Itâs not experimental anymoreâitâs production-ready. Use it for product galleries, navigation between pages, filtering lists, expanding cards, or any UI change where you want smooth motion instead of instant replacement.
The View Transitions API handles the animation complexity so you donât have to. No more manual position tracking, no more complex state management, no more animation libraries for basic transitions. The browser does it all.