🎉 hey, I shipped skillcraft.ai — it shows you which dev skills are in demand

Thought you might find it useful. See what's trending, what's fading, and which skills are getting people hired.

Published
8 min read

View Transitions API: Smooth animations between DOM states

Create animated transitions between different states of your app without complex animation libraries.

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 finish
await 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.

Welcome

This demo shows custom slide transitions between steps.

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.

1TypeScript
2React
3Vue
4Svelte
5Angular

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:

Terminal window
npm install react@experimental react-dom@experimental

Don’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 Router
navigate('/new-page');
// becomes
document.startViewTransition(() => {
navigate('/new-page');
});
// With Vue Router
router.push('/new-page');
// becomes
document.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.


Found this article helpful? You might enjoy my free newsletter. I share dev tips and insights to help you grow your coding skills and advance your tech career.


Check out these related articles that might be useful for you. They cover similar topics and provide additional insights.


This article was originally published on https://www.trevorlasn.com/blog/view-transitions-api. It was written by a human and polished using grammar tools for clarity.