Articles /

New featureanimation

View Transitions API

How to animate between page states and DOM updates using the browser's native transition system.

What view transitions do

When content changes on a page — a navigation, a list reorder, a detail view opening — users lose context if the change is instant. JavaScript animation libraries (Framer Motion, GSAP, etc.) handle this, but they require capturing old state, positioning elements, and orchestrating timelines.

The View Transitions API does this natively. You call document.startViewTransition(callback), update the DOM in the callback, and the browser cross-fades (or morphs) between the old and new states automatically. CSS controls the animation.

Basic usage — SPA navigation

// JavaScript: wrap DOM update in startViewTransition
async function navigate(url) {
  const content = await fetch(url).then(r => r.text());

  document.startViewTransition(() => {
    document.querySelector('main').innerHTML = content;
  });
}

// CSS: override the default cross-fade
::view-transition-old(root) {
  animation: slide-out 0.3s ease-in;
}
::view-transition-new(root) {
  animation: slide-in 0.3s ease-out;
}
@keyframes slide-out { to   { translate: -100% 0; opacity: 0; } }
@keyframes slide-in  { from { translate:  100% 0; opacity: 0; } }

The browser captures a screenshot of the old state, runs your callback (which updates the DOM), then animates between old and new. If no CSS is provided, it defaults to a cross-fade.

Named elements — shared element transitions

By naming elements with view-transition-name, you tell the browser to morph a specific element between the old and new states. This is the "shared element transition" pattern — like clicking a card thumbnail and having it expand into the detail view.

/* CSS: name the element on both "pages" */
.card-thumbnail { view-transition-name: hero-image; }
.detail-hero    { view-transition-name: hero-image; }

/* Browser morphs between them automatically */

/* Customize the morph animation */
::view-transition-group(hero-image) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

Names must be unique in the DOM at any given time. Assign the same name to the element in the "before" state and the element in the "after" state, and the browser handles the morph.

Cross-document transitions (MPA)

For traditional multi-page apps, the @view-transition rule enables same-origin transitions across full page loads.

/* In your CSS — on both the outgoing and incoming page */
@view-transition {
  navigation: auto;
}

/* Customize the cross-page animation */
::view-transition-old(root) {
  animation: fade-out 0.25s ease;
}
::view-transition-new(root) {
  animation: fade-in 0.25s ease;
}

This works for same-origin navigations. Support is in Chrome 126+ and is expanding to other browsers. No JavaScript required for the basic cross-fade.

Accessibility and reduced motion

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.01ms;
  }
}

Always reduce or disable view transitions for users who prefer reduced motion. The API provides a transition.ready promise so you can conditionally skip: if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { await callback(); return; }.

View Transitions: Chrome 111+ (SPA), Chrome 126+ (MPA). Use as progressive enhancement — the DOM update works regardless; the transition is the layer on top.