View Transitions Playground

dominic.balass.one

The View Transitions API turns a hard DOM swap into an animated transition with one function call: document.startViewTransition(() => { /* your DOM update */ }). The browser snapshots the before and after states and tweens between them. Below is a tiny travel journal. Click a destination: the photo and the title morph into the detail view. Flip the transitions toggle to feel what the API adds, or try the slide and zoom presets.

Support check (live)

checking… checking…

Same-document transitions are Baseline: Chrome/Edge 111+, Safari 18+, Firefox 138+. Cross-document transitions (@view-transition { navigation: auto; }) are newer: Chromium 126+, Safari 18.2+, Firefox still trailing. This demo uses the same-document API, and it skips transitions entirely when you prefer reduced motion.

The demo

Preset:

Wander. a tiny travel journal

data-vt-preset values

The demo picks its panel animation by setting a data-vt-preset attribute on the <html> element just before each transition. The CSS keys each animation off that attribute, so only the matching ::view-transition-old/new rules apply. Valid values:

Live: <html> currently has (absent)

Why not just CSS?

CSS can animate an element. It cannot animate a change. The moment you swap content, the old DOM is gone, so there is nothing left to animate out. Doing this in pure CSS means keeping two copies of the content alive at once, animating between them, then cleaning up. That is the FLIP pattern, and the measuring and cleanup is where the bugs live.

This API snapshots the page before your DOM update and after it, then hands you both snapshots as pseudo-elements you can animate. No duplicate live DOM. No measuring rectangles. The shared-element morph is one view-transition-name on both ends and the browser does the rest. The slide preset above is about ten lines of CSS.

And the fallback is free. No support means the update just happens instantly. It is polish, not a dependency.

The code

1. The whole basic API

document.startViewTransition(() => {
  // any DOM update: swap views, re-render a list, toggle a class
  showDetail(place);
});

2. Shared-element morph (photo and title fly into place)

// give both ends the SAME name; each name must be unique per snapshot
thumbImg.style.viewTransitionName = 'wander-img-amalfi';
titleEl.style.viewTransitionName = 'wander-title-amalfi';
// ...render the detail view with the same two names...
const t = document.startViewTransition(() => showDetail('amalfi'));
t.finished.finally(() => {
  thumbImg.style.viewTransitionName = '';
  titleEl.style.viewTransitionName = '';
});

3. Custom animation, pure CSS

::view-transition-old(wander) {
  animation: slide-out 380ms ease-in both;
}
::view-transition-new(wander) {
  animation: slide-in 380ms ease-out both;
}
@keyframes slide-out { to { transform: translateX(-72px); opacity: 0; } }
@keyframes slide-in { from { transform: translateX(72px); } }

4. Feature-detect and respect reduced motion

const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

function update(fn) {
  if (document.startViewTransition && !reduce) {
    document.startViewTransition(fn);
  } else {
    fn(); // plain instant swap: the free fallback
  }
}

Important notes