View Transitions API Examples You Can Copy and Run
How to use the View Transitions API: same-document and cross-document examples, naming elements with view-transition-name, and fallbacks.
The View Transitions API lets the browser animate between two states of a page. You change the DOM inside a callback, and the browser snapshots the old view, snapshots the new one, and cross-fades them. For a single-page app you call document.startViewTransition(). For a multi-page site you opt in with the @view-transition CSS at-rule, and no JavaScript is needed for the default animation.
The smallest working version is three lines of JavaScript or one block of CSS. Everything after that, such as animating one element separately, changing the timing, or skipping the effect, is CSS on a handful of pseudo-elements. The rest of this post covers both modes, naming elements, framework questions, fallbacks, and the mistakes I'd watch for.
What the View Transitions API does#
Per MDN's overview, the API handles animated transitions between DOM states in a single-page app and between documents in a multi-page app. Before it existed, SPA transitions meant writing a lot of CSS and JavaScript to position old and new content, animate both, block stray interaction with the old content, and then remove it. Cross-document transitions were effectively impossible.
The browser now does that work. During a transition it renders an overlay on top of the page, built from these pseudo-elements:
| Pseudo-element | What it is |
|---|---|
::view-transition |
Root of the overlay, above all page content |
::view-transition-group() |
Root of a single transition |
::view-transition-image-pair() |
Container for the old and new views |
::view-transition-old() |
Static snapshot of the old view |
::view-transition-new() |
Live representation of the new view |
You style those with ordinary CSS animations. That's the whole mental model.
Same-document transitions (SPAs)#
Wrap the DOM change in document.startViewTransition(). The callback does the update, and the browser handles the snapshots.
function showPanel(id) {
document.startViewTransition(() => {
document.querySelector('.panel.active')?.classList.remove('active');
document.getElementById(id).classList.add('active');
});
}
That gives you the browser's default animation with no CSS at all. The method returns a ViewTransition object. It lets you react when the transition is ready to animate or finished, and you can skip it altogether. That's useful when you'd rather not animate under some conditions, for example when the user has asked for reduced motion.
To change the default animation, target the old and new snapshots of the page root:
::view-transition-old(root) {
animation: 200ms ease-out both fade-out;
}
::view-transition-new(root) {
animation: 300ms ease-in both fade-in;
}
@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }
If you want to see this working in a real app, MDN links a basic image-gallery demo that animates old and new images and captions separately.
Cross-document transitions (MPAs)#
For navigations between two real pages, both the current and the destination document have to opt in with @view-transition. Put this in the CSS of every page that should take part:
@view-transition {
navigation: auto;
}
The pages need to be same-origin for this to apply. Once both sides opt in, a link click runs the default cross-fade, and you customize it with the same pseudo-elements as above. MDN's two-page demo uses this to do a custom "swipe up" transition.
Two events give you JavaScript hooks:
pageswapfires on the page being left, just before it's unloaded. Its event object gives you theViewTransition, plus info about the navigation type and the current and destination history entries.pagerevealfires on the page being shown, when a document is first rendered. That covers a fresh network load and activation from the back/forward cache or a prerender. It also gives you theViewTransition, if the navigation triggered one.
window.addEventListener('pagereveal', (event) => {
if (!event.viewTransition) return; // no transition on this navigation
// adjust or skip it here, e.g. event.viewTransition.skipTransition()
});
There's also <link rel="expect">. It marks the most critical content in a document and blocks rendering until that content has been parsed. The point is a consistent first paint, and therefore a consistent transition, across supporting browsers. If your transition flickers because the new page paints half-built, I'd try this first.
Naming elements with view-transition-name#
By default the whole page cross-fades as one snapshot. To animate an element on its own, give it a view-transition-name. That element gets its own snapshot and moves independently of the rest of the page.
.hero-image {
view-transition-name: hero;
}
If the old state has an element named hero and the new state has one too, the browser morphs between them: position, size, and content. The classic use is a thumbnail in a list that grows into the full image on the detail view. You can then target it:
::view-transition-group(hero) {
animation-duration: 400ms;
}
A few related properties are worth knowing:
view-transition-classgives you another way to style a set of named elements, so you don't write a rule per name.- The
match-elementvalue ofview-transition-namelets the browser match individual elements, which suits animated list items. MDN has a demo of this. view-transition-scopeisolates the discoverability of named elements to a subtree. It's part of element-scoped view transitions, which MDN documents separately.:active-view-transitionand:active-view-transition-type()match while a transition (or one with specific types) is in progress. Handy for styling the rest of the page only during a transition.
Here's a small helper for the list-to-detail case. It generates a unique name per item, because names have to be unique, which I'll come back to.
const items = [{ id: 'a1' }, { id: 'b2' }, { id: 'a1' }];
const names = items.map((item) => `card-${item.id}`);
const dupes = names.filter((name, i) => names.indexOf(name) !== i);
console.log('names:', names);
console.log(dupes.length ? `duplicate names: ${dupes}` : 'all names unique');
Fallbacks for browsers without support#
The API is progressive enhancement by nature. The transition is decoration, and the DOM update is the actual feature. So feature-detect startViewTransition and run the update directly when it's missing.
function transition(doc, update) {
if (!doc.startViewTransition) {
update();
return 'no support: updated without animation';
}
doc.startViewTransition(update);
return 'animated';
}
const updates = [];
const supported = { startViewTransition(cb) { cb(); } };
const unsupported = {};
console.log(transition(supported, () => updates.push('first')));
console.log(transition(unsupported, () => updates.push('second')));
console.log('updates applied:', updates);
In a browser you'd pass document as the first argument. The point is that the update runs in both branches.
For cross-document transitions there's nothing to detect. Browsers that don't understand @view-transition ignore the rule and navigate normally. For reduced motion, I'd wrap your custom animations in a prefers-reduced-motion media query or skip the transition from script.
For the current support picture, check the compatibility tables on MDN for Document.startViewTransition and the @view-transition at-rule, or look at Can I use. Support changes, and a table you check today beats a claim I'd make here.
View Transitions API in React, Next.js, Angular and Astro#
The source material I worked from covers the browser API, not any framework's wrapper. So I'll stick to what holds everywhere: the underlying calls are document.startViewTransition() for same-document changes and @view-transition for cross-document ones, regardless of framework.
For a client-rendered framework like React, the one thing to get right is that the callback you pass must make the DOM change by the time the browser takes the new snapshot. If your framework applies state updates asynchronously, calling setState inside the callback may not have touched the DOM yet. Check your framework's current docs, such as react.dev, for how it integrates with the API before you build on it. For server-rendered sites that do full page loads, the CSS opt-in route needs no framework involvement at all, and I'd try it first.
Common mistakes and gotchas#
- Duplicate names. Each
view-transition-namemust identify one element at a time. If two visible elements share a name, the transition can't pair them properly. Generate names per item, as in the helper above. - Opting in on only one page. Cross-document transitions need both the current and destination documents to opt in with
@view-transition. - Treating the transition as the feature. If the DOM update lives only inside an unsupported API call, older browsers get nothing. Always run the update in the fallback path.
- Styling the wrong snapshot.
::view-transition-old()is a static snapshot, while::view-transition-new()is live. Animate them separately, and don't expect the old one to react to anything. - Inconsistent first paint on MPAs. If the new page renders before its key content is parsed, the transition starts from the wrong state.
<link rel="expect">blocks rendering until the critical content is in. - Forgetting reduced motion. The
ViewTransitionobject lets you skip the animation, so use it when the user prefers less motion.
Quick reference#
| Goal | Use |
|---|---|
| Animate a DOM change in an SPA | document.startViewTransition(callback) |
| Animate navigation between pages | @view-transition { navigation: auto; } on both pages |
| Animate one element on its own | view-transition-name on that element |
| Style many named elements at once | view-transition-class |
| Customize old or new snapshot | ::view-transition-old() / ::view-transition-new() |
| Hook into a page being left or shown | pageswap / pagereveal events |
| Stabilize first paint on MPAs | <link rel="expect"> |
Before shipping:
- Feature-detect
startViewTransitionand run the update either way. - Check that every
view-transition-nameon screen is unique. - For MPAs, confirm both pages include the
@view-transitionrule. - Respect reduced-motion preferences.
- Check the MDN and Can I use tables for the browsers you support.
The full API reference is on MDN, including the demos mentioned above.
Quick answers
How do I use the View Transitions API?
For a single-page app, call document.startViewTransition() with a callback that updates the DOM. For a multi-page site, add the @view-transition at-rule with navigation: auto to both pages' CSS.
Is the View Transitions API supported in Firefox and other browsers?
Support varies by browser and by feature, and it changes over time. Check the compatibility tables for Document.startViewTransition and the @view-transition at-rule on MDN, or look it up on Can I use. Feature-detect and fall back to a plain DOM update.
Can I use the View Transitions API with React, Next.js, Angular or Astro?
The underlying API is the same in any framework. The main thing is making sure your DOM update has actually happened inside the callback you pass to startViewTransition(). Check your framework's current docs for its own integration.
How do I animate just one element?
Give it a view-transition-name in CSS. The element gets its own snapshot, and you can style it with ::view-transition-group(), ::view-transition-old() and ::view-transition-new() using that name.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base. Keeps the notes free and the examples running.

0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
On a computer? Scan the code with your phone wallet, or copy the address.
USDC or ETH on Base works best. Other tokens on Base, Ethereum or Arbitrum also reach this address. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note