# View Transitions API

> How document.startViewTransition() snapshots the DOM and animates to the new state, the ViewTransition promises, the @view-transition rule, and engine support.

`document.startViewTransition(updateCallback)` captures the current page as a set of
snapshots, runs the callback that changes the DOM, captures the new state, and animates
between the two with CSS, defaulting to a cross-fade. The same mechanism applies to
cross-document navigations when both pages opt in with the `@view-transition` rule, which gives a
multi-page PWA the animated route changes that previously required a client-side router.

## Syntax

```js
document.startViewTransition(updateCallback)
document.startViewTransition({ update, types })
transition.updateCallbackDone
transition.ready
transition.finished
transition.skipTransition()
```

```css
@view-transition { navigation: auto; }
```

The method returns a `ViewTransition` synchronously. Same-document transitions are in Chrome
111, Safari 18, and Firefox 144 (BCD `api.Document.startViewTransition`); the cross-document
`@view-transition` rule is in Chrome 126 and Safari 18.2 and absent from Firefox (BCD
`css.at-rules.view-transition`).

## Parameters

| Parameter | Type | Meaning |
|---|---|---|
| `updateCallback` | function returning a value or promise | Changes the DOM. The old snapshot is captured before it runs and the new one after its returned promise settles; rendering is paused in between, so keep it short. |
| `options.update` | function | Same as `updateCallback`, in the object form. |
| `options.types` | array of strings | Level 2: transition types matched by `:active-view-transition-type()` so one stylesheet can animate "forward" and "back" differently. |
| `transition.updateCallbackDone` | `Promise<void>` | Settles when the callback's promise settles. |
| `transition.ready` | `Promise<void>` | Settles when the pseudo-elements exist and the animation is about to start; the point to attach a Web Animations API animation. Rejects when the transition is skipped. |
| `transition.finished` | `Promise<void>` | Settles after the animation ends and the pseudo-elements are removed. |

The animation runs on a pseudo-element tree rooted at `::view-transition`: for each
`view-transition-name` (the default `root` plus every element given one in CSS) there is a
`::view-transition-group(name)` holding a `::view-transition-image-pair(name)` with
`::view-transition-old(name)` (a bitmap of the old state) and `::view-transition-new(name)` (a
live view of the new one). The group animates position and size between the two states; the old
and new images cross-fade; all of it is restyled with ordinary CSS animations.

## Exceptions

| Exception | When |
|---|---|
| `InvalidStateError` `DOMException` | Two elements share a `view-transition-name` at capture time. The transition is skipped, `ready` rejects, and the DOM update still applies. |
| `TimeoutError` `DOMException` | The update callback's promise takes longer than the engine's limit (4 s in Chromium); the transition is skipped and the update applies. |
| `AbortError` `DOMException` | A second `startViewTransition()` call starts before the first finishes, skipping the first; `skipTransition()` rejects `ready` the same way. |
| Any error thrown by `updateCallback` | Rejects `updateCallbackDone`, `ready`, and `finished`; the DOM is left as the callback left it. |

A transition that is skipped is not a failure of the update: the callback's changes are applied
in every case above, which is what makes the API safe to call unconditionally once
feature-detected.

## Examples

The single-page example animates a shared element; the multi-page one needs only CSS; the last is the guard.

### Animating a list-to-detail navigation in a single-page app

The thumbnail and the detail hero share a `view-transition-name` set just before the
transition, so the group animates the image from one place to the other instead of cross-fading
the whole page.

```js
async function openDetail(item) {
  const thumb = document.querySelector(`[data-id="${item.id}"] img`);
  thumb.style.viewTransitionName = 'hero';
  const transition = document.startViewTransition(async () => {
    await renderDetail(item); // replaces the list with the detail view
    document.querySelector('.detail img').style.viewTransitionName = 'hero';
  });
  await transition.finished;
  thumb.style.viewTransitionName = ''; // one element per name at the next capture
}
```

Clearing the name afterwards matters: a later transition that finds two `hero` elements is
skipped with `InvalidStateError`.

```css
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 250ms;
}
::view-transition-group(hero) {
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
```

The root cross-fade and the hero group are styled independently; without the first rule the
default cross-fade lasts 250 ms as well, and the second only changes the hero's easing.

### Opting a multi-page app into cross-document transitions

Both the outgoing and the incoming page carry the rule; the browser captures the old page on
`pageswap` and the new one on `pagereveal`, and the same pseudo-elements animate. Same-origin
navigations only.

```css
@view-transition {
  navigation: auto;
}
header {
  view-transition-name: site-header;
}
```

Giving the header a name keeps it still while the rest of the page cross-fades; a prerendered
next page (see [speculation rules](/reference/performance/speculation-rules/)) makes the
transition start without a network wait.

### Detecting support and skipping the animation

The method is absent in engines without the API. The fallback applies the DOM change directly,
which is the same end state without the animation.

```js
function transitionTo(update) {
  if (!('startViewTransition' in document)) {
    return update(); // no View Transitions: update without animation
  }
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
    return update(); // user asked for less motion: skip the animation
  }
  return document.startViewTransition(update).finished;
}
```

Returning `finished` lets callers await the end of the animation before, for example,
focusing the new view's heading.

:::observed
While a transition is running, Chrome DevTools, Elements panel, shows the pseudo-element tree
as children of `<html>`: `::view-transition`, then `::view-transition-group(root)`,
`::view-transition-image-pair(root)`, `::view-transition-old(root)`, and
`::view-transition-new(root)`, with one more `::view-transition-group(…)` per named element
([Same-document view transitions for single-page applications](https://developer.chrome.com/docs/web-platform/view-transitions/same-document),
developer.chrome.com). Pausing the Animations panel during the transition keeps the tree on
screen so each group's styles can be inspected; once `finished` settles, the pseudo-elements
disappear from the tree.
:::

## See also

- [CSS View Transitions Module Level 1](https://www.w3.org/TR/css-view-transitions-1/) (w3.org)
- [CSS View Transitions Module Level 2](https://www.w3.org/TR/css-view-transitions-2/) (w3.org)
- [Same-document view transitions for single-page applications](https://developer.chrome.com/docs/web-platform/view-transitions/same-document) (developer.chrome.com)
- [Speculation Rules API](/reference/performance/speculation-rules/)
- [Back/forward cache (bfcache)](/reference/performance/bfcache/)
- [Navigation API](/reference/capabilities/navigation-api/)