CoreMesh

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.

javascript
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:

css
::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:

css
@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:

  • pageswap fires on the page being left, just before it's unloaded. Its event object gives you the ViewTransition, plus info about the navigation type and the current and destination history entries.
  • pagereveal fires 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 the ViewTransition, if the navigation triggered one.
javascript
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.

css
.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:

css
::view-transition-group(hero) {
  animation-duration: 400ms;
}

A few related properties are worth knowing:

  • view-transition-class gives you another way to style a set of named elements, so you don't write a rule per name.
  • The match-element value of view-transition-name lets the browser match individual elements, which suits animated list items. MDN has a demo of this.
  • view-transition-scope isolates the discoverability of named elements to a subtree. It's part of element-scoped view transitions, which MDN documents separately.
  • :active-view-transition and :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.

javascript
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.

javascript
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-name must 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 ViewTransition object 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:

  1. Feature-detect startViewTransition and run the update either way.
  2. Check that every view-transition-name on screen is unique.
  3. For MPAs, confirm both pages include the @view-transition rule.
  4. Respect reduced-motion preferences.
  5. 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.

Saved you an hour? Send a tip.

USDC or ETH on Base. Keeps the notes free and the examples running.

Base
QR code with the CoreMesh tip address on Base
0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
Open in wallet · 5 USDC

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

`code` and ``` blocks work

No account needed. Comments are read before they show up, usually the same day. Your email is never asked for.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS