Part 4 of 4 · CSS Shine Lab

The View Transition API

Animate between two views, a crossfade or a shared-element morph, by wrapping a DOM change so the browser tweens the old state into the new one.

  • Innovation

Reach for the View Transition API when the change is bigger than a single element: swapping a whole panel, or moving between pages. Support is still uneven. Same-document transitions ship in Chrome and Safari, cross-document navigation ships in Chromium, and Firefox is still catching up. It degrades gracefully, though, so you can use it today and browsers that do not support it simply skip the animation.

Implementation

There are two ways in. In a single document, wrap a DOM change in document.startViewTransition(() => { /* update the DOM */ }) and the browser snapshots before and after and animates between them: a crossfade by default, or a morph for any element you give a unique view-transition-name.

Across pages you do not need JavaScript at all. Opt in with @view-transition { navigation: auto } and give matching view-transition-name values to elements on both pages.

For single-page apps, resist hand-wiring startViewTransition into your router. React is adding a dedicated <ViewTransition> component for exactly this, and it is still experimental, so it is better to wait for it to stabilise than to build something you will have to replace.

Crossfade a change

The simplest transition. Wrap a DOM update in startViewTransition and, with no view-transition-name anywhere, the whole change is captured by the default root group and old crossfades to new.

Panel A Panel B
View source

HTML

<div class="vt-swap" id="vt-swap">
  <span class="vt-swap__a">Panel A</span>
  <span class="vt-swap__b">Panel B</span>
</div>
<button
  type="button"
  class="vt-swap-toggle"
  onclick="
    const swap = () => document.getElementById('vt-swap').classList.toggle('is-b');
    document.startViewTransition ? document.startViewTransition(swap) : swap();
  "
>
  Swap panels
</button>

CSS

.vt-swap {
  display: grid;
  place-items: center;
  min-height: 6rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: 1.25rem;
}

.vt-swap.is-b {
  background: #7c3aed;
  color: #fff;
}

/* Show one panel at a time; the button below toggles `is-b`. */
.vt-swap__b {
  display: none;
}

.vt-swap.is-b .vt-swap__a {
  display: none;
}

.vt-swap.is-b .vt-swap__b {
  display: block;
}

.vt-swap-toggle {
  margin-top: 0.75rem;
  padding: 0.6rem 1.1rem;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

/* No view-transition-name anywhere, so the change is captured by the default
   `root` group and old simply crossfades to new. */

This is not just a demo. The theme switch in this site’s footer does exactly this: toggling between light and dark wraps the change in document.startViewTransition, so the whole page crossfades instead of flashing.

Where a browser does not support it yet, the switch just happens instantly. That is the point: it costs almost nothing to add, and the browsers that support it get the crossfade for free. Try it, bottom right.

Morph a named element

Give an element a unique view-transition-name and it becomes its own transition group, so instead of crossfading it smoothly morphs between its old and new size and position.

View source

HTML

<div class="vt-morph" id="vt-morph">
  <div class="vt-morph__box"></div>
</div>
<button
  type="button"
  class="vt-morph-toggle"
  onclick="
    const move = () => document.getElementById('vt-morph').classList.toggle('is-end');
    document.startViewTransition ? document.startViewTransition(move) : move();
  "
>
  Move and resize
</button>

CSS

.vt-morph {
  display: flex;
  justify-content: flex-start;
}

.vt-morph.is-end {
  justify-content: flex-end;
}

.vt-morph__box {
  width: 5rem;
  height: 5rem;
  border-radius: var(--radius-md);
  background: var(--color-accent);

  /* A unique name makes the box its own transition group, so it smoothly morphs
     between its old and new size/position instead of crossfading. */
  view-transition-name: vt-morph-box;
}

.vt-morph.is-end .vt-morph__box {
  width: 8rem;
  height: 8rem;
  background: #7c3aed;
}

.vt-morph-toggle {
  margin-top: 0.75rem;
  padding: 0.6rem 1.1rem;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

Back to CSS Shine Lab