CSS View Transitions: From Basic to Advanced

Quick Answer

The View Transitions API is a browser-native way to animate between two visual states of a page. The browser snapshots the old state, lets you update the DOM, then crossfades or morphs from the snapshot to the new live state using CSS pseudo-elements. Trigger same-document transitions with document.startViewTransition(cb), and enable cross-document (MPA) transitions with the CSS at-rule @view-transition { navigation: auto; }. Customize the result with standard CSS animations.

For years, smooth page-to-page transitions were the exclusive domain of JavaScript frameworks and single-page applications. Native multi-page websites were stuck with hard cuts between pages — a jarring experience compared to the polished animations users had come to expect from mobile apps. The View Transitions API changes everything. It provides a browser-native mechanism for animating between DOM states, whether you are updating content within a single page or navigating between entirely separate HTML documents. The API is specified by the CSS View Transitions Module Level 1 and Level 2 (cross-document) drafts.

At its core, a view transition captures a snapshot of the current visual state, applies the DOM change, then animates from the old snapshot to the new live state. The browser handles the heavy lifting: creating pseudo-elements that represent the outgoing and incoming states, managing layering, and even providing default crossfade animations that work out of the box. You can customize every aspect of the animation using standard CSS, which means the skills you already have transfer directly. For full reference material see MDN: View Transitions API.

This guide walks through every layer of the View Transitions API, starting with same-document transitions in single-page apps, moving into cross-document transitions for traditional multi-page sites, and concluding with advanced patterns like shared element transitions and framework integration. By the end, you will have the knowledge to add fluid, accessible transitions to any web project.

What View Transitions Are

A view transition is a coordinated animation between two visual states of the page. When you trigger a transition, the browser performs the following sequence: first, it captures screenshots of elements you have tagged for transitioning. Then it allows the DOM to update. Finally, it creates a tree of pseudo-elements that crossfade from the captured screenshots (the "old" state) to the newly rendered elements (the "new" state). This entire process is managed by the browser's compositor, which means transitions run at 60fps even if the main thread is busy with JavaScript.

The default animation is a simple crossfade — the old content fades out while the new content fades in simultaneously. However, because the transition is driven by CSS pseudo-elements, you can replace the default crossfade with slides, scales, morphs, or any other CSS animation you can imagine.

Same-Document Transitions with document.startViewTransition()

Same-document view transitions are designed for single-page applications and any scenario where you update the DOM without a full page navigation. You trigger them by calling document.startViewTransition() and passing a callback that performs the DOM update.

// Basic same-document view transition
function updateContent(newHTML) {
  const transition = document.startViewTransition(() => {
    document.querySelector('.content').innerHTML = newHTML;
  });

  // The transition object gives you promises to track the animation
  transition.finished.then(() => {
    console.log('Transition complete');
  });
}

The callback you pass can be synchronous or asynchronous. If it returns a Promise, the browser waits for that Promise to resolve before capturing the new state and starting the animation. This is critical for scenarios like fetching new content from an API — you want the new data to be rendered before the transition begins.

// Async transition with data fetching
async function navigateTo(url) {
  const transition = document.startViewTransition(async () => {
    const response = await fetch(url);
    const html = await response.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    document.querySelector('main').replaceWith(
      doc.querySelector('main')
    );
  });
}

If the browser does not support view transitions, document.startViewTransition will be undefined. You should always feature-detect before calling it, falling back to an immediate DOM update for unsupported browsers.

function updateView(callback) {
  if (!document.startViewTransition) {
    callback();
    return;
  }
  document.startViewTransition(callback);
}

The ::view-transition Pseudo-Element Tree

When a view transition runs, the browser constructs a tree of pseudo-elements overlaid on top of the page. Understanding this tree is essential for customizing animations. The structure looks like this:

::view-transition
  ::view-transition-group(root)
    ::view-transition-image-pair(root)
      ::view-transition-old(root)
      ::view-transition-new(root)

The ::view-transition pseudo-element is the top-level overlay that covers the entire viewport. Beneath it, each transitioning element gets a group. By default, the entire page is captured as a single group called root. Inside each group is an image-pair container that holds two children: ::view-transition-old (a screenshot of the element before the DOM change) and ::view-transition-new (a live rendering of the element after the DOM change).

The ::view-transition-group animates the size and position from the old state to the new state. The ::view-transition-old and ::view-transition-new pseudo-elements handle the crossfade — old fades from opacity: 1 to opacity: 0, and new does the reverse.

Customizing Animations with ::view-transition-old and ::view-transition-new

The default crossfade is pleasant but generic. You can replace it with any CSS animation by targeting the pseudo-elements. For example, here is a slide-in-from-right transition:

/* Slide the old content out to the left */
::view-transition-old(root) {
  animation: slide-out 0.3s ease-in both;
}

/* Slide the new content in from the right */
::view-transition-new(root) {
  animation: slide-in 0.3s ease-out both;
}

@keyframes slide-out {
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

@keyframes slide-in {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
}

You can also control the transition duration, timing function, and delay on the ::view-transition-group pseudo-element. The group controls the interpolation of size and position, so changing its animation-duration affects how quickly a shared element morphs from its old location to its new one.

Cross-Document (MPA) Transitions

Cross-document view transitions extend the same concept to multi-page applications — traditional websites where each navigation loads an entirely new HTML document. This is enabled with a CSS at-rule rather than a JavaScript API. Both the outgoing and incoming pages must opt in:

/* Add this to every page that should participate in transitions */
@view-transition {
  navigation: auto;
}

When a user clicks a link and both the current page and the destination page include this rule, the browser captures the old page, loads the new page, then runs the view transition animation between them. The same pseudo-element tree is created, and you customize the animations with the same CSS pseudo-elements described above.

Cross-document transitions work with same-origin navigations only. The browser will skip the transition for cross-origin links, prerendered pages that fail, or navigations triggered by form submissions (unless explicitly handled). The transition also respects the pageswap and pagereveal events, which fire on the outgoing and incoming pages respectively, giving you JavaScript hooks to customize behavior per-navigation.

One important constraint: cross-document transitions require that the new page renders quickly. If the incoming page takes too long to load, the browser cancels the transition and shows the new page immediately. This is by design — the API should never make navigation feel slower.

Named View Transitions with view-transition-name

By default, the entire page is treated as a single transitioning element with the name root. Named view transitions let you break the page into independently animating pieces. You assign a view-transition-name to any element, and the browser will create a separate group in the pseudo-element tree for that element.

/* Give the header its own transition group */
.site-header {
  view-transition-name: header;
}

/* Give the main content area its own group */
.main-content {
  view-transition-name: main;
}

/* Now you can animate them independently */
::view-transition-old(header) {
  animation: none; /* Header stays put */
}
::view-transition-new(header) {
  animation: none;
}

::view-transition-old(main) {
  animation: fade-and-slide-out 0.4s ease-in;
}
::view-transition-new(main) {
  animation: fade-and-slide-in 0.4s ease-out;
}

Every view-transition-name must be unique on the page at the time the transition runs. If two elements share the same name, the transition will fail. This uniqueness requirement is what enables the browser to match elements across states — it finds the element with name "header" in the old state and the element with name "header" in the new state, then creates a group that morphs between them.

Shared Element Transitions: Thumbnail to Hero Pattern

One of the most visually compelling uses of view transitions is the shared element pattern, where an element appears to fly from its position on one page to its position on another. The classic example is a product thumbnail in a grid that expands into a full-size hero image on the detail page.

To achieve this, you assign the same view-transition-name to the thumbnail on the list page and the hero image on the detail page. The browser automatically animates the position, size, and aspect ratio between the two states.

/* List page: thumbnail */
.product-card img {
  view-transition-name: product-hero;
}

/* Detail page: hero image */
.product-hero {
  view-transition-name: product-hero;
}

/* Optionally smooth the group animation */
::view-transition-group(product-hero) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

For list pages where each item needs a unique name, you can generate them dynamically. In HTML you might use inline styles: style="view-transition-name: product-42". In frameworks, you can compute the name from the item's ID. Just remember: only one element with that name should be visible at a time.

Framework Integration

Vanilla JavaScript

In a vanilla JavaScript application using client-side routing, wrap your route-change logic in document.startViewTransition(). This is the most straightforward approach. Intercept link clicks, fetch the new page content, and swap it inside the transition callback.

Astro

Astro provides built-in support for view transitions through its <ViewTransitions /> component. Add it to your layout's <head> and Astro automatically uses cross-document view transitions when navigating between pages. You can use the transition:name directive to set view-transition-name on elements, and the transition:animate directive to choose preset animations like slide, fade, or morph.

Next.js

Next.js does not yet have first-class view transition support built into the App Router, but you can implement same-document transitions by wrapping your navigation in document.startViewTransition(). Use the useRouter hook combined with startTransition from React, and call document.startViewTransition() around the router push. The community package next-view-transitions provides a drop-in <Link> component that automates this pattern.

Debugging with Chrome DevTools

Chrome DevTools provides dedicated tooling for inspecting view transitions. Open the Animations panel (under More Tools) and trigger a transition. You will see the pseudo-element tree appear in the Elements panel, and the Animations panel will show a timeline of all running animations. You can slow down animations to 25% or 10% speed to inspect them frame by frame.

The "Capture view transition pseudo-elements" checkbox in the Rendering panel lets you persist the pseudo-elements in the DOM tree after the transition completes, so you can inspect their styles at rest. This is invaluable for debugging positioning issues with named transitions.

If a transition is not firing, check the Console for errors. Common issues include duplicate view-transition-name values, the callback throwing an error, or the transition being skipped because the page was not visible (e.g., the tab was in the background).

Handling prefers-reduced-motion

View transitions are visual animations, and some users have motion sensitivity. You should always respect the prefers-reduced-motion media query. The simplest approach is to disable view transition animations entirely for users who prefer reduced motion:

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

This does not disable the DOM update — it just makes the visual transition instantaneous. The user still gets the new content; they simply do not see the crossfade or slide animation. This is the recommended approach because it preserves the functional behavior while removing the motion.

Alternatively, you can check the media query in JavaScript before calling startViewTransition(), and skip the transition entirely if the user prefers reduced motion. However, the CSS approach is generally cleaner because it works for both same-document and cross-document transitions.

Practical Patterns and Tips

Back/Forward Transitions

Users expect different animation directions for forward and backward navigation. You can detect navigation direction using the Navigation API's NavigateEvent.navigationType or by adding a data attribute to the document before the transition starts. Then use CSS to conditionally apply forward or backward slide animations.

Loading States

For same-document transitions with asynchronous data fetching, consider showing a loading indicator if the fetch takes longer than a threshold. The transition.ready promise resolves when the pseudo-elements are created and the animation is about to start, while transition.updateCallbackDone resolves when your callback completes. You can use the gap between calling startViewTransition() and updateCallbackDone resolving to show a spinner.

Avoiding Layout Shifts

Ensure that the new state has the same overall page dimensions as the old state, or that you have explicitly sized your transitioning elements. If the new content is significantly taller than the old content, the transition can look jarring as the page height snaps. Consider setting a min-height on your content container or using contain: layout to prevent unexpected size changes during the transition.

Browser Support

Same-document view transitions (document.startViewTransition()) are supported in Chrome 111+, Edge 111+, and Safari 18+. Firefox added support in version 132. Cross-document (MPA) view transitions with the @view-transition at-rule are supported in Chrome 126+, Edge 126+, and Safari 18+. Firefox support for cross-document transitions is in development.

Because view transitions are a progressive enhancement by nature, you can adopt them today without worrying about fallbacks. Browsers that do not support the API simply perform a normal, instant DOM update or page navigation. No content is lost and no functionality breaks.

Frequently Asked Questions

What is the View Transitions API?

The View Transitions API is a browser-native mechanism for animating between two visual states of a page. The browser snapshots the old state, lets you update the DOM, then crossfades or animates from the snapshot to the new live state using CSS pseudo-elements.

How do I trigger a view transition in a SPA?

Call document.startViewTransition(callback), where the callback updates the DOM. The callback may be async and return a Promise. Always feature-detect first, because the API is undefined in older browsers and you should fall back to running the callback immediately.

How do cross-document (MPA) view transitions work?

Add @view-transition { navigation: auto; } to the CSS of both the outgoing and incoming page. When a user follows a same-origin link the browser captures the old page, loads the new one, and runs the transition between them. Customize with ::view-transition-old() and ::view-transition-new().

What is view-transition-name used for?

It assigns a unique identifier to an element so the browser creates an independent animation group for it. Matching the same name across two states (e.g. a thumbnail and the corresponding hero image) lets the browser morph position, size, and aspect ratio between them — the shared-element transition pattern.

Which browsers support view transitions in 2026?

Same-document view transitions: Chrome 111+, Edge 111+, Safari 18+, Firefox 132+. Cross-document (MPA) view transitions: Chrome 126+, Edge 126+, Safari 18+. Firefox cross-document support is in development.

How do I respect prefers-reduced-motion?

Inside @media (prefers-reduced-motion: reduce), set animation-duration: 0s !important on ::view-transition-group(*), ::view-transition-old(*), and ::view-transition-new(*). The DOM still updates — only the animation is suppressed.

Further Reading

External references: MDN — View Transitions API · W3C — CSS View Transitions Module Level 1 · Chrome for Developers — Cross-document view transitions.