🚀AdvancedPerformance & UX

View Transitions API

Create smooth animated transitions between page states or routes using the View Transitions API without JavaScript animation libraries.

Overview

The View Transitions API enables smooth visual transitions between different states of your web application for example, navigating between pages, updating a list, or expanding a card using CSS animations controlled by the browser. It removes the need for complex JavaScript animation libraries for common UI transitions. For same-document transitions, wrap your DOM update in `document.startViewTransition(() => updateDOM())`. The browser takes a screenshot of the current state, applies your DOM change, then animates from old to new using CSS pseudo-elements (::view-transition-old and ::view-transition-new). By default, the transition is a cross-fade. For page navigations in multi-page apps (MPA), Chrome 126+ supports cross-document view transitions via the `@view-transition { navigation: auto }` CSS rule no JavaScript required for page-to-page transitions.

Key Points

  • •`document.startViewTransition(callback)` wrap DOM updates for a smooth transition
  • •Default transition: cross-fade between old and new state
  • •`::view-transition-old(root)` and `::view-transition-new(root)` are the CSS pseudo-elements to animate
  • •`view-transition-name: my-name` CSS property on elements enables per-element transitions (e.g., hero images)
  • •Named transitions allow specific elements to animate independently (morph from thumbnail to full image)
  • •For MPA page transitions: `@view-transition { navigation: auto }` in CSS (Chrome 126+)
  • •Reduce motion: always wrap view transition CSS in `@media (prefers-reduced-motion: no-preference)`

Examples

Basic same-document view transition

<div id="content">
  <h1>Page 1</h1>
  <button onclick="updateContent()">Go to Page 2</button>
</div>

<style>
  /* Override the default cross-fade for the root transition */
  @media (prefers-reduced-motion: no-preference) {
    ::view-transition-old(root) {
      animation: slide-out 0.3s ease-in forwards;
    }
    ::view-transition-new(root) {
      animation: slide-in 0.3s ease-out forwards;
    }
    @keyframes slide-out {
      to { transform: translateX(-100%); opacity: 0; }
    }
    @keyframes slide-in {
      from { transform: translateX(100%); opacity: 0; }
    }
  }
</style>

<script>
async function updateContent() {
  if (!document.startViewTransition) {
    // Fallback: no animation, just update
    document.getElementById('content').innerHTML = '<h1>Page 2</h1>';
    return;
  }

  const transition = document.startViewTransition(() => {
    document.getElementById('content').innerHTML = '<h1>Page 2</h1>';
  });

  await transition.finished;
  console.log('Transition complete');
}
</script>

Always provide a no-transition fallback for browsers that do not support startViewTransition.

Hero element transition (card expands to detail view)

<style>
  .card { view-transition-name: hero-card; }
  .detail { view-transition-name: hero-card; }

  @media (prefers-reduced-motion: no-preference) {
    /* Named elements animate from their old position to new position */
    ::view-transition-old(hero-card),
    ::view-transition-new(hero-card) {
      animation-duration: 0.4s;
      animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    }
  }
</style>

<div class="card" id="card" onclick="expandCard()">
  <img src="thumb.jpg" alt="Article thumbnail">
  <h3>Article Title</h3>
</div>

<script>
function expandCard() {
  document.startViewTransition(() => {
    document.getElementById('card').outerHTML = `
      <div class="detail">
        <img src="full.jpg" alt="Full article image">
        <h1>Article Title</h1>
        <p>Full article content...</p>
      </div>
    `;
  });
}
</script>

Both elements share the same view-transition-name the browser morphs between their shapes and positions.

MPA cross-document view transition

/* In your CSS (e.g., styles.css) applies to all pages */

/* Enable automatic transitions for all navigations */
@view-transition {
  navigation: auto;
}

/* Customize: slide instead of fade for page navigations */
@media (prefers-reduced-motion: no-preference) {
  @keyframes slide-from-right {
    from { transform: translateX(100%); }
  }
  @keyframes slide-to-left {
    to { transform: translateX(-100%); }
  }

  /* Apply to forward navigation */
  ::view-transition-new(root) {
    animation: slide-from-right 0.3s ease;
  }
  ::view-transition-old(root) {
    animation: slide-to-left 0.3s ease;
  }
}

With @view-transition navigation: auto, ALL page navigations on your site get this transition for free.

Common Mistakes

  • Not wrapping view transition CSS in `@media (prefers-reduced-motion: no-preference)` animations must respect the user's motion preference
  • Assigning the same view-transition-name to two different visible elements simultaneously names must be unique among currently visible elements
  • Not providing a fallback when `document.startViewTransition` is undefined Safari and older browsers don't support it

Note

The transition pseudo-elements live in a separate layer above all page content. This means `z-index` on your page elements does not affect the transition animation layer you can't accidentally put a header on top of the transition.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What does document.startViewTransition(callback) do?