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 ::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.

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 states 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 § 13 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):

2.2. Isolating Scoped View Transition Names: the view-transition containment value

Name: contain
New values: view-transition

The contain: view-transition value establishes view transition name containment on an element. This causes any document-scoped view transition names to only be visible for view transitions scoped to this element or its descendants.

Note: Most elements running a scoped view transition should use view transition name containment, to ensure that their document-scoped view transition names don’t accidentally interfere with other scoped view transitions. Otherwise, two similar elements running the same scoped view transition, expecting the same-named descendants, will see each other’s names and abort (since view transitions don’t allow multiple elements to have the same view-transition-name).

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 ViewTransition’s root element. All of the view transition pseudo-elements are selected from their ultimate originating element, the ViewTransition’s root element.

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

For example, even though a ::view-transition-group() pseudo-element is nested underneath its parent ::view-transition pseudo-element for styling and layout purposes, it’s selected directly on the element hosting the ViewTransition: if you’d use .foo::view-transition, you’ll also use .foo::view-transition-group().

Doing .foo::view-transition::view-transition-group() is incorrect, and won’t select anything.

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. Scoped View Transitions

In addition to view transitions triggered on the document itself (global view transitions), individual elements can host their own scoped view transitions. This enables multiple view transitions to run on a page at the same time, with each animating a different subtree of the document. It also enables a view transition to be affected by the layout and rendering of its subtree, being affected by scrolling, filters, z-index, etc. ("Global" view transitions are always rendered on a separate, higher rendering layer than the rest of the document.)

In order to host a scoped view transition, an element must have layout containment, so its painted output can be captured as an atomic unit.

View transitions whose root element is the document element are not scoped view transitions. They’re global view transitions.

5. View Transition Layout

The view transition pseudo-elements are styled, laid out, and rendered like normal elements. Global view transitions 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; scoped view transitions are simply rendered after and above all other children of their root element.

5.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 snapshot containing block on a mobile OS. The snapshot includes the URL bar, as this can be scrolled away. The keyboard is included as this appears and disappears. The top and bottom bars are part of the OS rather than the browser, so they’re not included in the snapshot containing block.
A diagram of a desktop browser window, including a tab bar, a URL bar, and a web-content area featuring both horizontal and vertical scrollbars The previous diagram, but highlights the area that's the 'snapshot containing block', which includes the web content area and the scrollbars
An example of the snapshot containing block on a desktop OS. This includes the scrollbars, but does not include the URL bar, as web content never appears in that area.

The snapshot containing block origin refers to the top-left corner of the snapshot containing block.

The snapshot containing block size refers to the width and height of the snapshot containing block as a tuple of two numbers.

The snapshot containing block is considered to be an absolute positioning containing block and a fixed positioning containing block for ::view-transition and its descendants.

5.2. View Transition Painting Order

This specification introduces a new stacking layer, the view transition layer, to the end of the painting order established in CSS2§E Elaborate Description of Stacking Contexts. [CSS2]

Fold this into [CSS-POSITION-4], as the interaction with top layers needs to be specified.

For a global view transition, the ::view-transition pseudo-element generates a new stacking context, called the view transition layer, which paints after all other content of the document (including any content rendered in the top layer), after any filters and effects that are applied to such content. (It is not subject to such filters or effects, except insofar as they affect the rendered contents of the ::view-transition-old() and ::view-transition-new() pseudo-elements.)

Note: The intent of the feature is to be able to capture the contents of the page, which includes the top layer elements. In order to accomplish that, the view transition layer cannot be a part of the captured stacking contexts, since that results in a circular dependency. Therefore, the view transition layer is a sibling of all other content.

When a Document’s active view transition’s phase is "animating", the boxes generated by any element in that Document with captured in a view transition and its element contents, except transition root pseudo-element’s inclusive descendants, are not painted (as if they had visibility: hidden) and do not respond to hit-testing (as if they had pointer-events: none).

Note: Elements participating in a transition need to skip painting in their DOM location because their image is painted in the corresponding ::view-transition-new() pseudo-element instead. Similarly, hit-testing is skipped because the element’s DOM location does not correspond to where its contents are rendered. However, there is no change in how these elements are accessed by assistive technologies or the accessibility tree.

6. User Agent Stylesheet

The global view transition user agent style sheet is a user-agent origin style sheet containing the following rules:

:root {
  view-transition-name: root;
}

:root::view-transition {
  position: fixed;
  inset: 0;
}

:root::view-transition-group(*) {
  position: absolute;
  top: 0;
  left: 0;

  animation-duration: 0.25s;
  animation-fill-mode: both;
}

:root::view-transition-image-pair(*) {
  position: absolute;
  inset: 0;
}

:root::view-transition-old(*),
:root::view-transition-new(*) {
  position: absolute;
  inset-block-start: 0;
  inline-size: 100%;
  block-size: auto;
}

:root::view-transition-image-pair(*),
:root::view-transition-old(*),
:root::view-transition-new(*) {
  animation-duration: inherit;
  animation-fill-mode: inherit;
  animation-delay: inherit;
  animation-timing-function: inherit;
  animation-iteration-count: inherit;
  animation-direction: inherit;
  animation-play-state: inherit;
}

:root::view-transition-group-children(*) {
  position: absolute;
  inset: 0;
  border-style: solid;
  border-color: transparent;
}

/* Default cross-fade transition */
@keyframes -ua-view-transition-fade-out {
  to { opacity: 0; }
}
@keyframes -ua-view-transition-fade-in {
  from { opacity: 0; }
}

/* Keyframes for blending when there are 2 images */
@keyframes -ua-mix-blend-mode-plus-lighter {
  from { mix-blend-mode: plus-lighter }
  to { mix-blend-mode: plus-lighter }
}
Explanatory Summary This UA style sheet does several things:

Additional styles are dynamically added to the user-agent origin during a view transition through the dynamic view transition style sheet.

7. Cross-document view transitions

7.1. Overview

This section is non-normative.

7.1.1. Activation

With same-document view transitions, the author starts a view transition using JavaScript, by calling startViewTransition. In cross-document view transition, what triggers a view transition is a navigation between two documents, as long as the following conditions are met:

See the lifecycle section for more details.

7.1.2. Waiting for the new state to stabilize

In same-document view transitions, the author can indicate when the new state of the transition is in a stable state by using the callback passed to startViewTransition. Since cross-document view transitions are declarative, there is no such explicit promise. Instead, the user agent relies on the render-blocking mechanism to decide when the document has reached a stable state. In this way, the author can use the blocking attribute, to delay the transition until:

Note: overusing the render-blocking mechanism could make it so that the old state remains frozen for a long time, resulting in a jarring user experience. To avoid this, it’s advised to ensure that the render-blocking elements are available in a timely manner.

In this example, the last frame of the old document will be shown, and the animation will be delayed, until all the following conditions are met:

<!DOCTYPE html>
<html>
  <head>
    < !-- This will be render-blocking by default -->
    <link rel="stylesheet" href="style.css">

    < !-- Since this script fixes up the layout, marking it as render blocking will
        ensure it's run before the view transition is activated -->
    <script async href="fixup.js" blocking="render"></script>

    < !-- Wait until the main-article element is seen and fully parsed before
        activating the transition -->
    <link rel="expect" href="#main-article" blocking="render">
  </head>
  <body>
    <header>...</header>
    <main>
      <article id="main-article">...</article>
    </main>
    <article id="secondary-article">...</article>
  </body>
</html>

7.1.3. Customization

The ViewTransition object enables customizing the transition in script. Same-document view transitions use a single ViewTransition object returned from the startViewTransition call for the entire lifecycle. Cross-document view transitions have two ViewTransition objects, one in the old document and one in the new document.
7.1.3.1. Handling the view transition in the old document

The pageswap event is fired at the last moment before a document is about to be unloaded and swapped by another document. It can be used to find out whether a view transition is about to take place, customize it using types, make last minute changes to the captured elements, or skip it if necessary. The PageSwapEvent interface has a viewTransition object, which would be non-null when the navigation is eligible to a view transition, and a activation object, providing handy information about the navigation, like the URL after redirects. The transition’s finished promise can be used for cleaning up after the transition, in case the document is later restored from BFCache.

7.1.3.2. Handling the view transition in the new document

The pagereveal event is fired right before presenting the first frame of the new document. It can be used to find out if the view transition is still valid, by querying the viewTransition attribute. Similar to a same-document view transition, the author can now select different types, make last minute changes to the captured elements, wait for the transition to be ready in order to animate it, or skip it altogether.

7.1.4. Lifecycle

This section is non-normative.

A successful cross-document view transition goes through the following phases:

  1. In the old Document:

    1. The user initiates a navigation, by clicking a link, submitting a form, pressing the browser’s back button, etc.

      Note: some navigations do not trigger a view-transition, e.g. typing a new address in the URL bar.

    2. When the new Document is ready to be activated, the pageswap event is fired.

    3. If the navigation is same origin, has no cross-origin redirects, and the old Document has opted in to cross-document view transitions, the event’s viewTransition attribute would be a ViewTransition object.

    4. The author can now customize the transition, e.g. by mutating its types, or skip it altogether.

    5. If the ViewTransition is not skipped, the state of the old document is captured.

    6. The navigation proceeds: the old Document is unloaded, and the new Document is now active.

  2. Then, in the new Document:

    1. When the new Document is ready for its first rendering opportunity, an event named pagereveal is fired on the new Document, with a viewTransition attribute.

    2. This ViewTransition’s updateCallbackDone promise is already resolved, and its captured elements are populated from the old Document.

    3. This is another opportunity for the author to customize the transition, e.g. by mutating its types, or skip it altogether.

    4. The state of the new document is captured as the "new" state of the transition.

    5. From this point forward, the transition continues in a similar fashion to a same-document transition, as per activate view transition.

7.2. Examples

To generate the same cross-fade as in the first example CSS View Transitions 1 § 1.6 Examples, but across documents, we don’t need JavaScript.

Instead, we opt in to triggering view-transitions on navigation in both page 1 and page 2:

// in both documents:
@view-transition {
  navigation: auto;
}

A link from page 1 to or from page 2 would generate a crossfade transition for example 1. To achieve the effect examples 2, 3 & 4, simply put the CSS for the pseudo-elements in both documents.

Note that the @view-transition rule can be used together with media queries. For example, this would only perform the transition when the screen width is greater than:
@view-transition {
  navigation: auto;
}

@media (max-width: 600px) {
  navigation: none;
}
To achieve the effect in example 5, we have to do several things:

In both pages:

@view-transition {
  navigation: auto;
}
In the old page:
addEventListener('click', event => {
  sessionStorage.setItem("lastClickX", event.clientX);
  sessionStorage.setItem("lastClickY", event.clientY);
});

In the new page:

// This would run both on initial load and on reactivation from BFCache.
addEventListener("pagereveal", async event => {
  if (!event.viewTransition)
    return;

  const x = sessionStorage.getItem("lastClickX") ?? innerWidth / 2;
  const y = sessionStorage.getItem("lastClickY") ?? innerHeight / 2;

  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  await event.viewTransition.ready;

  // Animate the new document's 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',
      pseudoElement: '::view-transition-new(root)'
    }
  );
})
To choose which elements are captured based on properties of the navigation, and whether certain images are loaded:

In the old page:

window.addEventListener("pageswap", event => {
  // For example, the page was hidden, or the navigation is cross-document.
  if (!event.viewTransition)
    return;

  // If you don't want view transition for back/forward navigations...
  if (event.activation.navigationType === "traverse") {
    event.viewTransition.skipTransition();
  }

  const newURL = new URL(event.activation.entry.url);
  if (newURL.pathname === "/details" && thumbnail.complete) {
    thumbnail.classList.add("transition-to-hero");

    // This will cleanup the state if the page is restored from BFCache.
    event.viewTransition.finished.then(() => {
      thumbnail.classList.remove("transition-to-hero");
    });
  }

});

In the new page:

window.addEventListener("pagereveal", event => {
  // For example, the page was hidden, the navigation is cross-document, or the transition was skipped in the old document.
  if (!event.viewTransition)
    return;

  const oldURL = new URL(navigation.activation.from.url);
  if (newURL.pathname === "/list") {
    event.viewTransition.types.add("coming-from-list");

    // This will show the thumbnail until the view transition is finished.
    if (!hero.complete) {
      setToThumbnail(hero);
      event.viewTransition.finished.then(() => {
        setToFullResolution(hero);
      })
    }
  }
});

7.3. Opting in to cross-document view transitions

7.3.1. The @view-transition rule

The @view-transition rule is used by a document to indicate that cross-document navigations should setup and activate a ViewTransition.

The @view-transition rule has the following syntax:

@view-transition {
  <declaration-list>
}

The @view-transition rule accepts the navigation and types descriptors.

Note: as per default behavior, the @view-transition rule can be nested inside a conditional group rule such as @media or @supports.

When the @view-transition rule changes for Document document, the user agent must update the opt-in state for outbound transitions given document.

Note: this needs to be cached in the boolean because the result needs to be read in parallel, when navigating.

7.3.2. The navigation descriptor

Name: navigation
For: @view-transition
Value: auto | none
Initial: none

The 'navigation' descriptor opts in to automatically starting a view transition when performing a navigation of a certain type. Must be present on both the old and new document.

none

There will be no transition.

auto

The transition will be enabled if the navigation is same-origin, without cross-origin redirects, and whose NavigationType is

Note: Navigations excluded from auto are for example, navigating via the URL address bar or clicking a bookmark, as well as any form of user or script initiated reload.

This at-rule conforms with the forward-compatible parsing requirement of CSS; conformant parsers that don’t understand these rules will ignore them without error. Any descriptors that are not recognized or implemented by a given user agent, or whose value does not match the grammars given here or in a future version of this specification, must be ignored in their entirety; they do not make the @view-transition rule invalid.

7.3.3. Accessing the @view-transition rule using CSSOM

The CSSViewTransitionRule represents a @view-transition rule.

[Exposed=Window]
interface CSSViewTransitionRule : CSSRule {
  readonly attribute CSSOMString navigation;
  [SameObject] readonly attribute FrozenArray<CSSOMString> types;
};

The navigation getter step is to return the value of the corresponding navigation descriptor if one exists, otherwise the empty string.

The types getter steps is to return the value of the corresponding types descriptor if one exists, otherwise an empty list.

8. Selective view transitions

8.1. Overview

This section is non-normative.

For simple pages, with a single view transition, setting the view-transition-name property on participating elements should be sufficient. However, in more complex scenarios, the author might want to declare various view transitions, and only run one of them simultaneously. For example, sliding the whole page when clicking on the navigation bar, and sorting a list when one of its items is dragged.

To make sure each view transition only captures what it needs to, and different transitions don’t interfere with each other, this spec introduces the concept of active types, alongside the :active-view-transition and :active-view-transition-type() pseudo-classes.

:active-view-transition matches the document element when it has an active view transition, and :active-view-transition-type() matches the document element if the types in the selectors match the active view transition’s active types.

The ViewTransition’s active types are populated in one of the following ways:

  1. Passed as part of the arguments to startViewTransition(callbackOptions)

  2. Mutated at any time, using the transition’s types

  3. Declared for a cross-document view transition, using the types descriptor.

8.2. Examples

For example, the developer might start a transition in the following manner:
document.startViewTransition({update: updateTheDOMSomehow, types: ["slide-in", "reverse"]});

This will activate any of the following selectors:

:root:active-view-transition-type(slide-in) {}
:root:active-view-transition-type(reverse) {}
:root:active-view-transition-type(slide-in, reverse) {}
:root:active-view-transition-type(slide-in, something-else) {}
:root:active-view-transition {}

While starting a transition without providing transition types, would only activate :active-view-transition:

document.startViewTransition(updateTheDOMSomehow);
// or
document.startViewTransition({update: updateTheDOMSomehow});
/* This would be active */
:root { }
:root:active-view-transition {}

/* This would not be active */
:root:active-view-transition-type(slide-in) {}
:root:active-view-transition-type(any-type-at-all-except-star) {}

8.3. Selecting based on the active view transition

8.3.1. The :active-view-transition pseudo-class

The :active-view-transition pseudo-class applies to the root element of the document, if it has an active view transition.

The specificity of an :active-view-transition is one pseudo-class selector.

An :active-view-transition pseudo-class matches the document element when its node document has an non-null active view transition.

8.3.2. The :active-view-transition-type() pseudo-class

The :active-view-transition-type() pseudo-class applies to the root element of the document, if it has a matching