Skip to content
Web animation enginesLesson 2 of 4Intermediate16 min

The View Transitions API

The View Transitions API animates between two DOM states — including a shared element that morphs across a navigation — with the browser doing the work.

Kept on this device only.

In one line: the View Transitions API lets you say "change the DOM, then animate the difference" — the browser snapshots the old and new states and tweens between them, including elements that should morph across a navigation, with almost none of the bookkeeping you'd write by hand.

What it is

A view transition animates the change from one DOM state to another. You hand the browser a callback that mutates the DOM; it takes a snapshot of the page before your change, applies the change, snapshots the page after, and then cross-fades (or otherwise animates) from the old picture to the new one.

There are two flavours. Same-document transitions cover SPA-style updates — you swap views inside one page and wrap the swap in document.startViewTransition. Cross-document transitions cover classic multi-page navigation (MPA) — two separate HTML documents, opted in with CSS, so even a server-rendered site can animate between pages.

The headline feature is shared elements. Tag an element in the old state and an element in the new state with the same view-transition-name, and the browser animates one into the other — a thumbnail growing into a hero image, a list row expanding into a detail panel — instead of fading them independently.

Why it matters

Continuity is the point. When a thing you tapped visibly becomes the thing you're now looking at, the user's eye tracks it and the navigation feels like one continuous space rather than two unrelated screens. That spatial continuity lowers cognitive load: less re-orienting, less "where did that go?"

It's also dramatically less code. The old way to morph one element into another was a manual FLIP animation — measure First and Last positions, compute the Invert transform, Play it back — repeated for every shared element and re-derived whenever layout changed. The View Transitions API replaces all of that with a name attribute and some CSS, and because it's native, the browser handles the snapshotting, the pseudo-element tree, and the compositing for you.

See it

Live demo
Tweak it3
320
Navigate between the list and the detail; watch the shared card keep its identity across the change.

Click a material to navigate from the list into its detail panel, then use ← Back to return. Switch the knob between shared element and crossfade: in shared-element mode the thumbnail keeps its identity and grows into the detail header; in crossfade mode the whole view simply fades from one to the other. This demo illustrates the concept with plain React state and CSS so you can feel the difference — the real document.startViewTransition code is in Build it below.

How it works

The same-document entry point is document.startViewTransition(updateDOM). You pass a callback that performs the DOM change; the browser captures a "before" snapshot, runs your callback, captures an "after" snapshot, and animates between them. It returns a ViewTransition object with promises (ready, finished, updateCallbackDone) so you can sequence work around the animation.

During the transition the browser builds a ::view-transition pseudo-element tree over the top of the page. For each named element (and for the root) you get a ::view-transition-group, a ::view-transition-image-pair, and inside it ::view-transition-old (the before snapshot) and ::view-transition-new (the after snapshot). The default animation cross-fades old into new and tweens the group's size and position; you target these pseudo-elements in CSS to customise timing, easing, or the animation itself.

Shared elements come from view-transition-name. Give an element a unique name in the old state and an element the same name in the new state, and the browser pairs them into one group and animates the old box into the new box — that's the morph. Names must be unique per snapshot.

Progressive enhancement matters: always feature-detect. document.startViewTransition doesn't exist in every browser, so guard on it (if (!document.startViewTransition)), and when it's missing just run the DOM update directly. The transition is an enhancement layered on top of a working state change — never a precondition for it.

Build it

The core is a wrapper that feature-detects, applies your DOM update inside startViewTransition when it can, and falls back to a plain update when it can't. The CSS names the shared element and tunes the animation — including a prefers-reduced-motion escape hatch.

A progressive-enhancement wrapper around startViewTransition
// Run `update` (a function that mutates the DOM) inside a view transition when
// supported; otherwise just call it. The DOM change happens either way.
function withViewTransition(update) {
  // Feature-detect: older browsers don't have this method at all.
  if (!document.startViewTransition) {
    update();
    return;
  }
  return document.startViewTransition(update);
}
 
// Example: navigating from a list to a detail view in an SPA.
function openDetail(item) {
  withViewTransition(() => {
    // Tag the element that should morph. The list thumbnail for this item
    // carries the SAME name, so the browser pairs and animates them.
    detailThumb.style.viewTransitionName = "hero";
    renderDetail(item);        // swap the view's DOM
    moveFocusToDetailHeading(); // a11y: send focus to the new view
  });
}

Make it yours

Use the controls beside the demo above to change transition style, duration, and timing function — each change updates the example live.

Experiment in the playground
  • Compare the two modes back to back: shared-element keeps the thumbnail's identity across the change, while crossfade treats the two views as unrelated pictures. Notice how much more "connected" the navigation feels with a shared element.
  • Open and back out a few times in a row — a good transition stays legible at speed and never gets in the way of the next action.
  • Imagine wiring the real API: which single element in your list deserves a view-transition-name? Usually it's the one thing the user's eye is already locked onto.

Reproduce it with an LLM

Reproduce it with an LLM

You are a senior front-end engineer. Use the View Transitions API to animate a same-document navigation between a list view and a detail view, with the tapped card acting as a shared element that morphs into the detail header. Wrap the DOM update in `document.startViewTransition(...)`, assign a matching `view-transition-name` to the shared card and its detail counterpart, and style `::view-transition-group/old/new` for a smooth morph. Provide a progressive-enhancement fallback: if `document.startViewTransition` is undefined, just apply the update directly. Disable the animation under `prefers-reduced-motion: reduce`. Return only the HTML, CSS, and JS.

Pitfalls & accessibility

  • Keep view-transition-name values unique within a snapshot. Two elements sharing a name at the same time is an error and the pairing silently breaks — generate per-item names (e.g. hero-${id}) rather than reusing one string across a list.

Further reading