CSS View Transitions Module Level 2

Editor’s Draft,

More details about this document
This version:
https://drafts.csswg.org/css-view-transitions-2/
Latest published version:
https://www.w3.org/TR/css-view-transitions-2/
Feedback:
CSSWG Issues Repository
Inline In Spec
Editors:
Noam Rosenthal (Google)
Khushal Sagar (Google)
Vladimir Levin (Google)
Tab Atkins-Bittner (Google)
Suggest an Edit for this Spec:
GitHub Editor

Abstract

This module defines the View Transition API, along with associated properties and pseudo-elements, which allows developers to create animated visual transitions representing changes in the document state.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Status of this document

This is a public copy of the editors’ draft. It is provided for discussion only and may change at any moment. Its publication here does not imply endorsement of its contents by W3C. Don’t cite this document other than as work in progress.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-view-transitions” in the title, like this: “[css-view-transitions] …summary of comment…”. All issues and comments are archived. Alternately, feedback can be sent to the (archived) public mailing list [email protected].

This document is governed by the 18 August 2025 W3C Process Document.

1. Introduction

This section is non-normative.

This specification introduces a DOM API and associated CSS features that allow developers to create animated visual transitions, called view transitions between different states of a document or an element, or between distinct same-origin documents.

1.1. Level 2 Updates

Level 2 defines the following, extending the model in [CSS-VIEW-TRANSITIONS-1]:

1.2. Separating Visual Transitions from DOM Updates

Traditionally, creating a visual transition between two document states required a period where both states were present in the DOM at the same time. In fact, it usually involved creating a specific DOM structure that could represent both states. For example, if one element was “moving” between containers, that element often needed to exist outside of either container for the period of the transition, to avoid clipping from either container or their ancestor elements.

This extra in-between state often resulted in UX and accessibility issues, as the structure of the DOM was compromised for a purely-visual effect.

View Transitions avoid this troublesome in-between state by allowing the DOM to switch between states instantaneously, then performing a customizable visual transition between the two states in another layer, using a static visual capture of the old state, and a live capture of the new state. These captures are represented as a tree of pseudo-elements (detailed in § 3.2 View Transition Pseudo-elements), where the old visual state co-exists with the new state, allowing effects such as cross-fading while animating from the old to new size and position.

1.3. View Transition Customization

By default, element.startViewTransition() and document.startViewTransition() create a view transition consisting of a cross-fade of the entire root element between the two DOM states.

Developers can instead choose which descendant elements are captured independently using the view-transition-name CSS property, allowing these to be animated independently of the rest of the page. Since the transitional state (where both old and new visual captures exist) is represented as pseudo-elements, developers can customize each transition using familiar features such as CSS Animations and Web Animations.

1.4. View Transition Lifecycle

A successful view transition goes through the following phases:

  1. Developer calls document.startViewTransition(updateCallback) or element.startViewTransition(updateCallback), which returns a ViewTransition, viewTransition.

  2. Current state captured as the “old” state.

  3. Rendering paused within the root element.

  4. Developer’s updateCallback function, if provided, is called, which updates the document state.

  5. viewTransition.updateCallbackDone fulfills.

  6. Current state captured as the “new” state.

  7. Transition pseudo-elements created. See § 3.2 View Transition Pseudo-elements for an overview of this structure.

  8. Rendering unpaused, revealing the transition pseudo-elements.

  9. viewTransition.ready fulfills.

  10. Pseudo-elements animate until finished.

  11. Transition pseudo-elements removed.

  12. viewTransition.finished fulfills.

1.5. Transitions as an enhancement

A key part of the View Transition API design is that an animated transition is a visual enhancement to an underlying document state change. That means a failure to create a visual transition, which can happen due to misconfiguration or device constraints, will not prevent the developer’s ViewTransitionUpdateCallback being called, even if it’s known in advance that the transition animations cannot happen.

For example, if the developer calls skipTransition() at the start of the view transition lifecycle, the steps relating to the animated transition, such as creating the view transition tree, will not happen. However, the ViewTransitionUpdateCallback will still be called. It’s only the visual transition that’s skipped, not the underlying state change.

Note: If the DOM change should also be skipped, then that needs to be handled by another feature. navigateEvent.signal is an example of a feature developers could use to handle this.

Although the View Transition API allows DOM changes to be asynchronous via the ViewTransitionUpdateCallback, the API is not responsible for queuing or otherwise scheduling DOM changes beyond any scheduling needed for the transition itself. Some asynchronous DOM changes can happen concurrently (e.g if they’re happening within independent components), whereas others need to queue, or abort an earlier change. This is best left to a feature or framework that has a more holistic view of the application.

1.6. Rendering Model

View Transition works by replicating an element’s rendered state using UA generated pseudo-elements. Aspects of the element’s rendering which apply to the element itself or its descendants, for example visual effects like filter or opacity and clipping from overflow or clip-path, are applied when generating its image in Capture the image.

However, properties like mix-blend-mode which define how the element draws when it is embedded can’t be applied to its image. Such properties are applied to the element’s corresponding ::view-transition-group() pseudo-element, which is meant to generate a box equivalent to the element.

If the ::view-transition-group() has a corresponding element in the "new" states, the browser keeps the properties copied over to the ::view-transition-group() in sync with the DOM element in the "new" state. If the ::view-transition-group() has corresponding elements both in the "old" and "new" state, and the property being copied is interpolatable, the browser also sets up a default animation to animate the property smoothly.

1.7. Examples

Taking a page that already updates its content using a pattern like this:
function spaNavigate(data) {
  updateTheDOMSomehow(data);
}

A view transition could be added like this:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

This results in the default transition of a quick cross-fade:

The cross-fade is achieved using CSS animations on a tree of pseudo-elements, so customizations can be made using CSS. For example:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

This results in a slower transition:

Building on the previous example, motion can be added:
@keyframes fade-in {
  from { opacity: 0; }
}

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

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Here’s the result:

Building on the previous example, the simple "whole document" transition displayed a visible judder in the purple heading bar, and had the heading text always slide to the left even when it was "moving" to the right in the 1->2 transition. To fix these issues, the header and text within the header can be given their own