10LOC
Chrome APIsintermediate

The View Transitions API for animated state changes

Published August 7, 2026

export const swapView = (activeEl: HTMLElement, render: () => void) => {
  if (!document.startViewTransition) {
    render();
    return;
  }

  activeEl.style.viewTransitionName = "active-view";
  const transition = document.startViewTransition(render);
  transition.finished.finally(() => {
    activeEl.style.viewTransitionName = "";
  });
};

What

document.startViewTransition(callback) snapshots the page, runs your DOM-update callback, snapshots the result, and animates between the two with a crossfade the browser generates — no manual keyframes.

Why it matters

Before this API, animating "the DOM changed" meant either CSS transitions on properties you controlled directly, or the FLIP technique: measure before, apply the change, measure after, then animate the delta with transform. FLIP works but it's boilerplate you rewrite for every new transition, and it can't cross unrelated layout changes (swapping display: none for display: block, reordering a list) without extra tricks.

startViewTransition sidesteps all of it. The browser takes a screenshot-like capture of the old state, lets your callback mutate the DOM however it wants — add elements, remove them, reorder them, flip display — takes a second capture, and cross-fades between the two captures by default. Opt individual elements into their own named transition (so they morph instead of crossfade) with view-transition-name, and the browser handles the rest.

How it works

The snippet's swapView wraps a render callback (whatever function actually mutates the DOM) in a transition:

  • It checks document.startViewTransition exists first — this API has no polyfill, so unsupported browsers must fall through to calling render() directly. Nothing looks broken; there's just no animation.
  • activeEl.style.viewTransitionName = "active-view" tags one element for its own transition group. The browser will look for an element with the same view-transition-name in both the before and after snapshots and morph between their positions/sizes, instead of just crossfading the whole page.
  • document.startViewTransition(render) runs render and returns a ViewTransition object with ready, updateCallbackDone, and finished promises.
  • transition.finished resolves once the animation (and any transition-triggered reflow) is done — that's the right point to clear the inline view-transition-name, so it doesn't collide with the next transition.

The actual animation is CSS you write separately: browsers generate ::view-transition-old(active-view) and ::view-transition-new(active-view) pseudo-elements you can target with normal transition/animation properties.

Gotchas

  • Two elements can't share the same view-transition-name at the same time — the transition silently aborts (with a console warning) if it finds a duplicate. Clear the name after use, as the snippet does.
  • This covers same-document transitions (SPA state changes). Cross-document transitions — animating between two full page loads — need a separate opt-in (@view-transition { navigation: auto } in both documents' CSS) and have narrower, more recent browser support than the same-document API.
  • Respect prefers-reduced-motion: wrap the pseudo-element animation rules in a media query, since startViewTransition runs the DOM update regardless — only the animation is presentational.
  • If render throws, the returned promises reject but the DOM change may be partially applied — don't rely on the transition for atomicity.

Browser support: same-document view transitions are Baseline-stable — Chrome 111+, Safari 18+, and Firefox's most recent stable releases. Cross-document transitions arrived later (Chrome 126+, Safari 18.2+) and are worth feature-detecting separately if you need them.

Related

  • CSS @starting-style — animates an element's first style update (e.g., mount); view transitions animate a change between two full DOM states. They compose well together.
  • The FLIP technique (First-Last-Invert-Play) — the manual, pre-2023 way to do the same "morph on layout change" effect this API replaces.