CSS View Transitions Module Level 1

Editor’s Draft,

More details about this document
This version:
https://drafts.csswg.org/css-view-transitions-1/
Latest published version:
https://www.w3.org/TR/css-view-transitions-1/
Implementation Report:
https://wpt.fyi/results/css/css-view-transitions
Feedback:
CSSWG Issues Repository
Editors:
Tab Atkins-Bittner (Google)
Jake Archibald (Google)
Khushal Sagar (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.

1.1. 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.2. View Transition Customization

By default, document.startViewTransition() creates a view transition consisting of a page-wide cross-fade between the two DOM states. Developers can also choose which 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.3. View Transition Lifecycle

A successful view transition goes through the following phases:

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

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

  3. Rendering paused.

  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.4. 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.5. 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.6. 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 header and text within the header can be given their own ::view-transition-group()s for the transition:
.main-header {
  view-transition-name: main-header;
}

.main-header-text {
  view-transition-name: main-header-text;
  /* Give the element a consistent size, assuming identical text: */
  width: fit-content;
}

By default, these groups will transition size and position from their “old” to “new” state, while their visual states cross-fade:

Building on the previous example, let’s say some pages have a sidebar:

In this case, things would look better if the sidebar was static if it was in both the “old” and “new” states. Otherwise, it should animate in or out.

The :only-child pseudo-class can be used to create animations specifically for these states:

.sidebar {
  view-transition-name: sidebar;
}

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

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

For cases where the sidebar has both an “old” and “new” state, the default animation is correct.

Not building from previous examples this time, let’s say we wanted to create a circular reveal from the user’s cursor. This can’t be done with CSS alone.

Firstly, in the CSS, allow the “old” and “new” states to layer on top of one another without the default blending, and prevent the default cross-fade animation:

::view-transition-image-pair(root) {
  isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

Then, the JavaScript:

// Store the last click event
let lastClick;
addEventListener('click', event => (lastClick = event));

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

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // Create a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          \`circle(0 at ${x}px ${y}px)\`,
          \`circle(${endRadius}px at ${x}px ${y}px)\`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

And here’s the result:

2. CSS properties

2.1. Tagging Individually Transitioning Subtrees: the view-transition-name property

Name: view-transition-name
Value: none | <custom-ident>
Initial: none
Applies to: all elements
Inherited: no
Percentages: n/a
Computed value: as specified
Canonical order: per grammar
Animation type: discrete

Note: though view-transition-name is discretely animatable, animating it doesn’t affect the running view transition. Rather, it’s a way to set its value in a way that can change over time or based on a timeline. An example for using this would be to change the view-transition-name based on scroll-driven animations.

The view-transition-name property “tags” an element for capture in a view transition, tracking it independently in the view transition tree under the specified view transition name. An element so captured is animated independently of the rest of the page.

none

The element will not participate independently in a view transition.

<custom-ident>

The element participates independently in a view transition—​as either an old or new element—​with the specified view transition name.

Each view transition name is a tree-scoped name.

Note: Since currently only document-scoped view transitions are supported, only view transition names that are associated with the document are respected.

The values none, auto, and match-element are excluded from <custom-ident> here.

Note: If this name is not unique (i.e. if two elements simultaneously specify the same view transition name) then the view transition will abort.

Note: For the purposes of this API, if one element has view transition name foo in the old state, and another element has view transition name foo in the new state, they are treated as representing different visual state of the same element, and will be paired in the view transition tree. This may be confusing, since the elements themselves are not necessarily referring to the same object, but it is a useful model to consider them to be visual states of the same conceptual page entity.

If the element’s principal box is fragmented, skipped, or not rendered, this property has no effect. See § 7 Algorithms for exact details.

To get the document-scoped view transition name for an Element element:
  1. Let scopedViewTransitionName be the computed value of view-transition-name for element.

  2. If scopedViewTransitionName is associated with element’s node document, then return scopedViewTransitionName.

  3. Otherwise, return none.

2.1.1. Rendering Consolidation

Elements captured in a view transition during a view transition or whose view-transition-name computed value is not none (at any time):

3. Pseudo-elements

3.1. Pseudo-element Trees

Note: This is a general definition for trees of pseudo-elements. If other features need this behavior, these definitions will be moved to [css-pseudo-4].

A pseudo-element root is a type of tree-abiding pseudo-element that is the root in a tree of tree-abiding pseudo-elements, known as the pseudo-element tree.

The pseudo-element tree defines the document order of its descendant tree-abiding pseudo-elements.

When a pseudo-element participates in a pseudo-element tree, its originating pseudo-element is its parent.

If a descendant pseudo of a pseudo-element root has no other siblings, then :only-child matches that pseudo.

Note: This means that ::view-transition-new(ident):only-child will only select ::view-transition-new(ident) if the parent ::view-transition-image-pair(ident) contains a single child. As in, there is no sibling ::view-transition-old(ident).

3.2. View Transition Pseudo-elements

The visualization of a view transition is represented as a pseudo-element tree called the view transition tree composed of the view transition pseudo-elements defined below. This tree is built during the setup transition pseudo-elements step, and is rooted under a ::view-transition pseudo-element originating from the root element. All of the view transition pseudo-elements are selected from their ultimate originating element, the document element.

The view transition tree is not exposed to the accessibility tree.

For example, the ::view-transition-group() pseudo-element is attached to the root element selector directly, as in :root::view-transition-group(); it is not attached to its parent, the ::view-transition pseudo-element.
Once the user-agent has captured both the “old” and “new” states of the document, it creates a structure of pseudo-elements like the following:
::view-transition
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
└─ …other groups…

Each element with a view-transition-name is captured separately, and a ::view-transition-group() is created for each unique view-transition-name.

For convenience, the document element is given the view-transition-name "root" in the user-agent style sheet.

Either ::view-transition-old() or ::view-transition-new() are absent in cases where the capture does not have an “old” or “new” state.

Each of the pseudo-elements generated can be targeted by CSS in order to customize its appearance, behavior and/or add animations. This enables full customization of the transition.

3.2.1. Named View Transition Pseudo-elements

Several of the view transition pseudo-elements are named view transition pseudo-elements, which are functional tree-abiding view transition pseudo-elements associated with a view transition name. These pseudo-elements take a <pt-name-selector> as their argument, and their syntax follows the pattern:

::view-transition-pseudo(<pt-name-selector>)

where <pt-name-selector> selects a view transition name, and has the following syntax definition:

<pt-name-selector> = '*' | <custom-ident>

A named view transition pseudo-element selector only matches a corresponding pseudo-element if its <pt-name-selector> matches that pseudo-element’s view transition name, i.e. if it is either * or a matching <custom-ident>.

Note: The view transition name of a view transition pseudo-element is set to the view-transition-name that triggered its creation.

The specificity of a named view transition pseudo-element selector with a <custom-ident> argument is equivalent to a type selector. The specificity of a named view transition pseudo-element selector with a * argument is zero.

3.2.2. View Transition Tree Root: the ::view-transition pseudo-element

The ::view-transition pseudo-element is a tree-abiding pseudo-element that is also a pseudo-element root. Its originating element is the document’s document element, and its containing block is the snapshot containing block.

Note: This element serves as the parent of all ::view-transition-group() pseudo-elements.

3.2.3. View Transition Named Subtree Root: the ::view-transition-group() pseudo-element

The ::view-transition-group() pseudo-element is a named view transition pseudo-element that represents a matching named view transition capture. A ::view-transition-group() pseudo-element is generated for each view transition name as a child of the ::view-transition pseudo-element, and contains a corresponding ::view-transition-image-pair().

This element initially mirrors the size and position of the “old” element, or the “new” element if there isn’t an “old” element.

If there’s both an “old” and “new” state, styles in the dynamic view transition style sheet animate this pseudo-element’s width and height from the size of the old element’s border box to that of the new element’s border box.

Also the element’s transform is animated from the old element’s screen space transform to the new element’s screen space transform.

This style is generated dynamically since the values of animated properties are determined at the time that the transition begins.

3.2.4. View Transition Image Pair Isolation: the ::view-transition-image-pair() pseudo-element

The ::view-transition-image-pair() pseudo-element is a named view transition pseudo-element that represents a pair of corresponding old/new view transition captures. This pseudo-element is a child of the corresponding ::view-transition-group() pseudo-element and contains a corresponding ::view-transition-old() pseudo-element and/or a corresponding ::view-transition-new() pseudo-element (in that order).

This element exists to provide isolation: isolate for its children, and is always present as a child of each ::view-transition-group(). This isolation allows the image pair to be blended with non-normal blend modes without affecting other visual outputs. As such, the developer would typically not need to add custom styles to the ::view-transition-image-pair() pseudo-element. Instead, a typical design would involve styling the ::view-transition-group(), ::view-transition-old(), and ::view-transition-new() pseudo-elements.

3.2.5. View Transition Old State Image: the ::view-transition-old() pseudo-element

The ::view-transition-old() pseudo-element is an empty named view transition pseudo-element that represents a visual snapshot of the “old” state as a replaced element; it is omitted if there’s no “old” state to represent. Each ::view-transition-old() pseudo-element is a child of the corresponding ::view-transition-image-pair() pseudo-element.

:only-child can be used to match cases where this element is the only element in the ::view-transition-image-pair().

The appearance of this element can be manipulated with object-* properties in the same way that other replaced elements can be.

Note: The content and natural dimensions of the image are captured in capture the image, and set in setup transition pseudo-elements.

Note: Additional styles in the dynamic view transition style sheet added to animate these pseudo-elements are detailed in setup transition pseudo-elements and update pseudo-element styles.

3.2.6. View Transition New State Image: the ::view-transition-new() pseudo-element

The ::view-transition-new() pseudo-element (like the analogous ::view-transition-old() pseudo-element) is an empty named view transition pseudo-element that represents a visual snapshot of the “new” state as a replaced element; it is omitted if there’s no “new” state to represent. Each ::view-transition-new() pseudo-element is a child of the corresponding ::view-transition-image-pair() pseudo-element.

Note: The content and natural dimensions of the image are captured in capture the image, then set and updated in setup transition pseudo-elements and update pseudo-element styles.

4. View Transition Layout

The view transition pseudo-elements are styled, laid out, and rendered like normal elements, except that they originate in the snapshot containing block rather than the initial containing block and are painted in the view transition layer above the rest of the document.

4.1. The Snapshot Containing Block

The snapshot containing block is a rectangle that covers all areas of the window that could potentially display page content (and is therefore consistent regardless of root scrollbars or interactive widgets). This makes it likely to be consistent for the document element’s old image and new element.

Within a child navigable, the snapshot containing block is the union of the navigable’s viewport with any scrollbar gutters.

A diagram of a phone screen, including a top status bar, a browser URL bar, web-content area with a floating scrollbar, a virtual keyboard, and a bottom bar with an OS back button The previous diagram, but highlights the area that's the 'snapshot containing block', which includes everything except the top status bar and the bottom bar with the OS back button
An example of the