document.startViewTransition()

View Transitions API

Wrap a DOM update in startViewTransition() and the browser screenshots the page before and after, then cross-fades between them. Give matching elements the same view-transition-name and it morphs their position and size too. Libraries used to fake that by measuring an element in both places and animating a clone between them.

The JS

Copied
function selectItem(id) {
  if (!document.startViewTransition) {
    setSelectedId(id);
    return;
  }
  document.startViewTransition(() => {
    flushSync(() => setSelectedId(id));
  });
}

The CSS

Copied
/* The same name on a card and on the detail view it opens into
   is what the browser morphs between. Set per item, so each card
   has its own: card-a, card-b, and so on. */
.gridItem,
.listItem,
.detail {
  view-transition-name: var(--vtn, none);
}

/* Both halves of the transition are pseudo-elements, so the timing
   is styled, not scripted. */
::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
}

How it works

Click a card to open its detail view, or switch between Grid and List. Both call document.startViewTransition(callback). The DOM updates synchronously inside the callback, then the browser diffs the old and new frames and animates the difference.

Elements sharing a view-transition-name across both frames get their own mini transition, morphing shape and position instead of cross-fading. That is why the clicked card grows into the detail panel instead of popping in.

The one demo here that needs JavaScript. A same-document transition has to be announced at the moment the DOM changes, and only a script knows when that is. The animation itself is still the browser’s: you style it through the ::view-transition-old() and ::view-transition-new() pseudo-elements. Between pages you don’t need a script at all, since @view-transition { navigation: auto } hands the whole thing over.

Limited support

Chrome / Edge111+Safari18+Firefox

Falls back to an instant swap in browsers without support.

:has() · tic-tac-toe

turns, wins and reset, all CSS

@property · counter()
hover me
cos() · sin()

trigonometry, in CSS

:has() quantity query

switch guests on