View Transitions API: App-Like Page Animations with Almost No JavaScript
Smooth animations between page states and between pages using the browser's View Transitions API, with plain JavaScript, CSS and React examples, browser support and accessibility.

Mohit Malaya Nandy
Software Engineer & Product Builder

On this page
Native apps feel smooth because screens animate into each other. On the web, that used to mean a heavy animation library and a lot of careful code. The View Transitions API lets the browser do it for you: take a snapshot, change the page, and animate between the two.
This guide covers both kinds of view transitions, animations inside one page and between separate pages, with copy-paste examples in plain JavaScript, CSS and React. It is based on the MDN documentation and current browser support data.
How it works#
When a transition starts, the browser does four things:
- Captures a snapshot of the current page (the "old" state).
- Runs your update, such as changing the DOM or loading a new page.
- Captures the "new" state.
- Animates from old to new. By default it cross-fades the whole page.
Your code only describes the change. The browser handles the snapshots and the animation, using GPU-friendly CSS animations you can customise.
| Type | Use it for | How you start it | Support |
|---|---|---|---|
| Same-document | Single-page apps: tabs, filters, lists, theme changes | document.startViewTransition(callback) | Chrome/Edge 111+, Safari 18+, Firefox 144+ |
| Cross-document | Multi-page sites: navigating between normal pages | CSS: @view-transition { navigation: auto; } | Chrome/Edge 126+, Safari 18.2+, partial in Firefox |
Support data from caniuse (same-document, cross-document). The good news: unsupported browsers just skip the animation, so this is a safe progressive enhancement.
Same-document transitions#
Your first transition
Wrap any DOM change in document.startViewTransition(). Fall back to a plain update when the API is missing:
function withTransition(update) {
if (!document.startViewTransition) {
update(); // older browsers: no animation, same result
return;
}
document.startViewTransition(update);
}
// Example: switch the grid layout
button.addEventListener('click', () => {
withTransition(() => {
gallery.classList.toggle('compact');
});
});That is all it takes to get a smooth cross-fade instead of an instant jump.
Animate specific elements
Give an element a view-transition-name, and the browser animates its position and size separately from the rest of the page. If a card moves from one spot to another, it slides there instead of fading:
.featured-card {
view-transition-name: featured-card; /* must be unique on the page */
}
/* Customise that element's animation */
::view-transition-group(featured-card) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}For lists where every item can move, give each item its own name, for example with an inline style such as style="view-transition-name: item-42". Names must be unique while visible, or the browser skips the transition.
The pseudo-elements you can style
| Pseudo-element | What it is |
|---|---|
| ::view-transition | The overlay that holds the whole animation |
| ::view-transition-group(name) | Animates the size and position of one named element |
| ::view-transition-old(name) | A static snapshot of the old state |
| ::view-transition-new(name) | The live new state |
Use root as the name to style the default whole-page transition:
/* Slide the page instead of cross-fading */
::view-transition-old(root) {
animation: 0.3s ease-in both slide-out;
}
::view-transition-new(root) {
animation: 0.3s ease-out both slide-in;
}
@keyframes slide-out { to { transform: translateX(-30px); opacity: 0; } }
@keyframes slide-in { from { transform: translateX(30px); opacity: 0; } }Cross-document transitions (between pages)#
For a classic multi-page site, you do not need any JavaScript. Add this CSS to both pages (the page you leave and the page you arrive on), using the @view-transition rule:
@view-transition {
navigation: auto;
}Now clicking a link to another page on the same origin cross-fades between them. Add matching view-transition-name values on both pages to animate shared elements. A classic example is a thumbnail on a list page that grows into the hero image on the detail page:
/* list page */
.post-card:hover .thumb { view-transition-name: hero-image; }
/* detail page */
.post-hero img { view-transition-name: hero-image; }Using view transitions in React and Next.js#
The browser takes its "new" snapshot as soon as your callback finishes. React normally updates the DOM a little later, so use flushSync to apply the update immediately inside the callback:
'use client';
import { useState } from 'react';
import { flushSync } from 'react-dom';
export default function Tabs({ tabs }) {
const [active, setActive] = useState(0);
function select(index) {
if (!document.startViewTransition) {
setActive(index);
return;
}
document.startViewTransition(() => {
flushSync(() => setActive(index));
});
}
return (
<div>
{tabs.map((tab, i) => (
<button key={tab.label} onClick={() => select(i)} aria-pressed={active === i}>
{tab.label}
</button>
))}
<section style={{ viewTransitionName: 'tab-panel' }}>{tabs[active].content}</section>
</div>
);
}React is also adding a built-in <ViewTransition> component that handles this automatically for updates inside startTransition. At the time of writing it is available in React canary releases, so check the docs before relying on it in production.
Respect reduced motion#
Some people get dizzy or distracted by motion. Honour the prefers-reduced-motion setting by turning transitions off:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}Tips and gotchas#
- Keep them short. 200 to 400 milliseconds feels responsive. Longer transitions feel slow.
- Do not block on slow work. The page is frozen during the update callback. Fetch data first, then start the transition with the result.
- Use unique names. Duplicate visible
view-transition-namevalues cancel the transition. - Know the promises.
startViewTransition()returns an object withreadyandfinishedpromises, useful for running code when the animation starts or ends. - Check performance. Animating many named elements at once can be expensive on low-end phones. Test on a real device.
Frequently asked questions#
What is the View Transitions API?
It is a browser feature that animates between two states of a page, or between two pages, automatically. The browser takes a snapshot before and after a change and smoothly animates between them, with no animation library needed.
Which browsers support view transitions?
Same-document view transitions work in Chrome and Edge 111+, Safari 18+ and Firefox 144+. Cross-document transitions between pages work in Chrome and Edge 126+ and Safari 18.2+, with partial support in Firefox. Browsers without support simply update the page without the animation.
Do view transitions work with React and Next.js?
Yes. Wrap the state update in document.startViewTransition and use flushSync so React updates the DOM inside the callback. React also has a built-in ViewTransition component, currently available in canary releases.
Why does view-transition-name need to be unique?
The browser matches elements between the old and new state by name. If two visible elements share the same name at the same time, the transition is skipped.
Are view transitions accessible?
They can be, if you respect the prefers-reduced-motion setting. Disable or shorten the animations for users who ask for reduced motion.
Official references#
- View Transition API and Using the View Transition API, MDN
- startViewTransition(), @view-transition and view-transition-name, MDN
- Browser support: same-document and cross-document, caniuse
- <ViewTransition> and flushSync, React
Found this useful? You can support my writing on GitHub Sponsors or with a coffee.


