ESC

Type to search the knowledge base.

View Transitions API CSS Angle

CSS view transitions — ::view-transition pseudo-tree, view-transition-name, default crossfades, and reduced-motion-safe page morphs.

advanced3 min read
  • css
  • view-transitions

The View Transitions API snapshots the old and new UI states and crossfades or morphs between them. From the CSS angle you name elements, style the pseudo-element tree, and opt out under reduced motion. JS starts the transition (document.startViewTransition); CSS defines how it looks.

Docs: MDN View Transitions API, view-transition-name, Chrome guide.

Minimal same-document flow (JS + CSS)

function go(updateDom) {
  if (!document.startViewTransition) {
    updateDom();
    return;
  }
  document.startViewTransition(updateDom);
}
/* Default root transition is a crossfade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 200ms;
}

Naming elements to morph

.hero-title {
  view-transition-name: hero-title;
}

.hero-media {
  view-transition-name: hero-media;
}

Matching names between old and new states enable shared-element style transitions. Names must be unique in a document state (one element per name).

::view-transition-old(hero-media),
::view-transition-new(hero-media) {
  animation-duration: 280ms;
  animation-timing-function: ease;
  height: 100%;
  object-fit: cover;
}

Pseudo-element tree

Key players:

  • ::view-transition
  • ::view-transition-group(name)
  • ::view-transition-image-pair(name)
  • ::view-transition-old(name)
  • ::view-transition-new(name)

You animate these like normal pseudo-elements — opacity, transform — carefully.

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

::view-transition-old(root) {
  animation: 160ms ease both fade-out;
}

::view-transition-new(root) {
  animation: 160ms ease both fade-in;
}

MPA / cross-document (where supported)

@view-transition {
  navigation: auto;
}

Opt-in for same-origin navigations in supporting browsers. Still progressive enhancement.

Reduced motion and fallbacks

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Always ensure the DOM update path works with no view transitions API.

Practical product use

  • Soft navigation between list and detail (shared thumbnail name)
  • Theme toggle crossfade of root
  • Tab panels with restrained opacity transitions

Avoid circus morphs on dense data tables.

Interview out-loud

“View Transitions snapshot old and new states and animate between them. I set view-transition-name on shared elements, style ::view-transition-old/new, and call document.startViewTransition when supported. CSS controls duration and easing; reduced motion disables animations. It’s progressive enhancement for navigation and state changes.”

Footguns

  1. Duplicate view-transition-name values in one frame.
  2. Animating huge full-page screenshots on low-end devices.
  3. Forgetting fallback when API missing.
  4. Ignoring focus management after the DOM swap.
  5. Conflict with other full-screen fixed layers and z-index during transition.

List → detail shared element

.thumb { view-transition-name: var(--vt-name); }
/* set --vt-name uniquely per item via inline style or attribute selector */
function openItem(id) {
  const run = () => router.navigate(`/items/${id}`);
  if (document.startViewTransition) document.startViewTransition(run);
  else run();
}

Ensure the detail page image uses the same view-transition-name for the morph. Clear names when leaving the view if you recycle DOM nodes. Test reduced motion and missing API paths every time you add a transition.

Debugging morph glitches

If a shared element jumps: check that names match exactly, that only one node owns the name per state, and that images have similar aspect ratios. Huge size deltas can look like a zoom war — soften with shorter animations or skip shared elements and crossfade root only. Always verify keyboard focus after transition; the API does not manage focus for you.

Further reading

Related guides