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 between distinct same-origin documents.
1.1. Level 2 Updates
Level 2 defines the following, extending the model in [CSS-VIEW-TRANSITIONS-1]:
-
Cross-document view transitions, including the @view-transition rule and the algorithms that enable the cross-document view transition lifecycle.
-
Selective view transitions, a way to match styles based on the existence of an active view transition, and more specifically based on the active view transition being of a certain type.
-
Sharing styles between view transition pseudo-elements, a way to declare a style once, and use it for multiple view transition pseudo-elements. This includes the view-transition-class property, and additions to named pseudo-elements
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, document.
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.startViewTransition()
1.4. View Transition Lifecycle
A successful view transition goes through the following phases:
-
Developer calls
document.
, which returns astartViewTransition
(updateCallback
)ViewTransition
, viewTransition. -
Current state captured as the “old” state.
-
Rendering paused.
-
Developer’s
updateCallback
function, if provided, is called, which updates the document state. -
viewTransition.
fulfills.updateCallbackDone
-
Current state captured as the “new” state.
-
Transition pseudo-elements created. See § 3.2 View Transition Pseudo-elements for an overview of this structure.
-
Rendering unpaused, revealing the transition pseudo-elements.
-
viewTransition.
fulfills.ready
-
Pseudo-elements animate until finished.
-
Transition pseudo-elements removed.
-
viewTransition.
fulfills.finished
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.
is an example of a feature developers could use to handle this.navigateEvent
.signal
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
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 : 5 s ; }
This results in a slower transition:
@keyframes fade-in{ from{ opacity : 0 ; } } @keyframes fade-out{ to{ opacity : 0 ; } } @keyframes slide-from-right{ from{ transform : translateX ( 30 px ); } } @keyframes slide-to-left{ to{ transform : translateX ( -30 px ); } } ::view-transition-old ( root) { animation : 90 ms cubic-bezier ( 0.4 , 0 , 1 , 1 ) both fade-out, 300 ms cubic-bezier ( 0.4 , 0 , 0.2 , 1 ) both slide-to-left; } ::view-transition-new ( root) { animation : 210 ms cubic-bezier ( 0 , 0 , 0.2 , 1 ) 90 ms both fade-in, 300 ms cubic-bezier ( 0.4 , 0 , 0.2 , 1 ) both slide-from-right; }
Here’s the result:
.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:
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 ( 30 px ); } } /* Entry transition */ ::view-transition-new ( sidebar) :only-child{ animation : 300 ms cubic-bezier ( 0 , 0 , 0.2 , 1 ) both fade-in, 300 ms cubic-bezier ( 0.4 , 0 , 0.2 , 1 ) both slide-from-right; } /* Exit transition */ ::view-transition-old ( sidebar) :only-child{ animation : 150 ms cubic-bezier ( 0.4 , 0 , 1 , 1 ) both fade-out, 300 ms 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.
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 § 12 Algorithms for exact details.
Element
element:
-
Let scopedViewTransitionName be the computed value of view-transition-name for element.
-
If scopedViewTransitionName is associated with element’s node document, then return scopedViewTransitionName.
-
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):
-
Form a stacking context.
-
Form a backdrop root.
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.
::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().
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).
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.
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.
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.
4.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]
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.
5. 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.25 s ; 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:-
Lay out ::view-transition to cover the entire snapshot containing block so that each :view-transition-group() child can lay out relative to it.
-
Give the root element a default view transition name, to allow it to be independently selected.
-
Reduce layout interference from the ::view-transition-image-pair() pseudo-element so that authors can essentially treat ::view-transition-old() and ::view-transition-new() as direct children of ::view-transition-group() for most purposes.
-
Inherit animation timing through the tree so that by default, the animation timing set on a ::view-transition-group() will dictate the animation timing of all its descendants.
-
Style the element captures ::view-transition-old() and ::view-transition-new() to match the size and position set on ::view-transition-group() (insofar as possible without breaking their aspect ratios) as it interpolates between them. Since the sizing of these elements depends on the mapping between logical and physical coordinates, dynamic view transition style sheet copies relevant styles from the DOM elements.
-
Set up a default quarter-second cross-fade animation for each ::view-transition-group().
Additional styles are dynamically added to the user-agent origin during a view transition through the dynamic view transition style sheet.
6. Cross-document view transitions
6.1. Overview
This section is non-normative.
6.1.1. Activation
With same-document view transitions, the author starts a view transition using JavaScript, by callingstartViewTransition
.
In cross-document view transition, what triggers a view transition is a navigation between two documents, as long as the following conditions are met:
-
Both documents are of the same origin;
-
The page is visible throughout the entire course of the navigation;
-
The user initiates the navigation by interacting with the page, e.g. by clicking a link or submitting a form; or by interacting with the browser UI to do a
traverse
navigation (back/forward). This excludes, for example, navigations initiated by the URL bar; -
the navigation didn’t include cross-origin redirects; and
-
both documents opted in to cross-document view transitions, using the @view-transition rule.
See the lifecycle section for more details.
6.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 tostartViewTransition
.
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:
-
All expected scripts are executed, by using the script’s
blocking
attribute on required scripts. -
All expected styles are executed, by using the style or link’s
blocking
attribute on required styles. -
All expected HTML elements are seen by the parser, using an
expect
HTMLLinkElement
element.
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:
-
style.css
is applied, to ensure the new state has the correct styles -
fixup.js
is run, to ensure the presentation is up to date with script-based fixups. -
The
main-article
section is seen and parsed, to ensure enough of the content is loaded before allowing the transition to proceed.
<!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 >
6.1.3. Customization
TheViewTransition
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.
6.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.
6.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.
6.1.4. Lifecycle
This section is non-normative.
A successful cross-document view transition goes through the following phases:
-
In the old
Document
:-
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.
-
When the new
Document
is ready to be activated, thepageswap
event is fired. -
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’sviewTransition
attribute would be aViewTransition
object. -
The author can now customize the transition, e.g. by mutating its
types
, orskip
it altogether. -
If the
ViewTransition
is not skipped, the state of the old document is captured. -
The navigation proceeds: the old
Document
is unloaded, and the newDocument
is now active.
-
-
Then, in the new
Document
:-
When the new
Document
is ready for its first rendering opportunity, an event namedpagereveal
is fired on the newDocument
, with aviewTransition
attribute. -
This
ViewTransition
’s
promise is already resolved, and its captured elements are populated from the oldupdateCallbackDone
Document
. -
This is another opportunity for the author to customize the transition, e.g. by mutating its
types
, orskip
it altogether. -
The state of the new document is captured as the "new" state of the transition.
-
From this point forward, the transition continues in a similar fashion to a same-document transition, as per activate view transition.
-
6.2. Examples
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.
@view-transition { navigation : auto; } @media ( max-width:600 px ) { navigation : none; }
-
Opt-in to navigation-triggered view-transitions in both pages.
-
Pass the click location to the new document, e.g. via
sessionStorage
. -
Intercept the
ViewTransition
object in the new document, using thepagereveal
event.
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)' } ); })
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); }) } } });
6.3. Opting in to cross-document view transitions
6.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.
6.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-
traverse
, or -
push
orreplace
, with user navigation involvement not equal to"browser UI"
.
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.
6.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.
7. Selective view transitions
7.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:
-
Passed as part of the arguments to
startViewTransition(callbackOptions)
-
Mutated at any time, using the transition’s
types
-
Declared for a cross-document view transition, using the types descriptor.
7.2. Examples
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) {}
7.3. Selecting based on the active view transition
7.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.
7.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 active view transition. It has the following syntax definition:
:active-view-transition-type(<custom-ident>#)
The specificity of an :active-view-transition-type() is one pseudo-class selector.
An :active-view-transition-type() pseudo-class matches the document element when its node document has an non-null active view transition, whose active types contains at least one of the <custom-ident> arguments.
7.4. Activating the transition type for cross-document view transitions
The types descriptor
Name: | types |
---|---|
For: | @view-transition |
Value: | none | <custom-ident>+ |
Initial: | none |
The 'types' descriptor sets the active types for the transition
when capturing or performing the transition, equivalent to calling startViewTransition(callbackOptions)
with that types
.
Note: the types descriptor only applies to the Document
in which it is defined.
The author is responsible for using their chosen set of types in both documents.
8. Sharing styles between view transition pseudo-elements
8.1. Overview
This section is non-normative.
When styling multiple elements in the DOM in a similar way, it is common to use the class attribute: setting a name that’s shared across multiple elements, and then using the class selector to declare the shared style.
The view transition pseudo-elements (e.g. view-transition-group()) are not defined in the DOM, but rather by using the view-transition-name property. For that purpose, the view-transition-class' CSS property provides view transitions with the equivalent of HTML classes. When an element with a view-transition-name also has a view-transition-class value, that class would be selectable by the pseudo-elements, as per the examples.
8.2. Examples
< div class = "box" id = "red-box" ></ div > < div class = "box" id = "green-box" ></ div > < div class = "box" id = "yellow-box" ></ div >
div.box{ view-transition-class : any-box; width : 100 px ; height : 100 px ; } #red-box{ view-transition-name : red-box; background : red; } #green-box{ view-transition-name : green-box; background : green; } #yellow-box{ view-transition-name : yellow-box; background : yellow; } /* The following style would apply to all the boxes, thanks to 'view-transition-class' */ ::view-transition-group ( *.any-box) { animation-duration : 1 s ; }
8.3. The view-transition-class property
Name: | view-transition-class |
---|---|
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 |
The view-transition-class can be used to apply the same style rule to multiple named view transition pseudo-elements which may have a different view-transition-name. While view-transition-name is used to match between the element in the old state with its corresponding element in the new state, view-transition-class is used only to apply styles using the view transition pseudo-elements (::view-transition-group(), ::view-transition-image-pair(), ::view-transition-old(), ::view-transition-new()).
Note that view-transition-class by itself doesn’t mark an element for capturing, it is only used as an additional way to style an element that already has a view-transition-name.
- none
-
No class would apply to the named view transition pseudo-elements generated for this element.
- <custom-ident>+
-
All of the specified <custom-ident> values (apart from none) are applied when used in named view transition pseudo-element selectors. none is an invalid <custom-ident> for view-transition-class, even when combined with another <custom-ident>.
Each 'view transition class' is a tree-scoped name.
Note: If the same view-transition-name is specified for an element both in the old and new states of the transition, only the view-transition-class values from the new state apply. This also applies for cross-document view transitions: classes from the old document would only apply if their corresponding view-transition-name was not specified in the new document.
8.4. Additions to named view transition pseudo-elements
The named view transition pseudo-elements (view-transition-group(), view-transition-image-pair(), view-transition-old(), view-transition-new()) are extended to support the following syntax:
::view-transition-group(<pt-name-and-class-selector>) ::view-transition-image-pair(<pt-name-and-class-selector>) ::view-transition-old(<pt-name-and-class-selector>) ::view-transition-new(<pt-name-and-class-selector>)
where <pt-name-selector> works as previously defined, and <pt-name-and-class-selector> has the following syntax definition:
<pt-name-and-class-selector> = <pt-name-selector> <pt-class-selector>? | <pt-class-selector> <pt-class-selector> = ['.' <custom-ident>]+
When interpreting the above grammar, white space is forbidden:
-
Between <pt-name-selector> and <pt-class-selector>
-
Between any of the components of <pt-class-selector>.
A named view transition pseudo-element selector which has one or more <custom-ident> values in its <pt-class-selector> would only match an element if the class list value in named elements for the pseudo-element’s view-transition-name contains all of those values.
The specificity of a named view transition pseudo-element selector with either:
-
a <pt-name-selector> with a <custom-ident>; or
-
a <pt-class-selector> with at least one <custom-ident>,
is equivalent to a type selector.
The specificity of a named view transition pseudo-element selector with a * argument and with an empty <pt-class-selector> is zero.
9. API
9.1. Additions to Document
partial interface Document {ViewTransition startViewTransition (optional (ViewTransitionUpdateCallback or StartViewTransitionOptions )= {} );
callbackOptions readonly attribute ViewTransition ?activeViewTransition ; };callback =
ViewTransitionUpdateCallback Promise <any > ();dictionary {
StartViewTransitionOptions ViewTransitionUpdateCallback ?=
update null ;sequence <DOMString >?=
types null ; };
viewTransition
=document
.startViewTransition
(updateCallback
)-
Starts a new view transition (canceling the
document
’s existing active view transition, if any).updateCallback
, if provided, is called asynchronously, once the current state of the document is captured. Then, when the promise returned byupdateCallback
fulfills, the new state of the document is captured and the transition is initiated.Note that
updateCallback
, if provided, is always called, even if the transition cannot happen (e.g. due to duplicateview-transition-name
values). The transition is an enhancement around the state change, so a failure to create a transition never prevents the state change. See § 1.5 Transitions as an enhancement for more details on this principle.If the promise returned by
updateCallback
rejects, the transition is skipped.
9.1.1. startViewTransition()
Method Steps
startViewTransition(callbackOptions)
are as follows:
-
Let updateCallback be null.
-
If callbackOptions is a
ViewTransitionUpdateCallback
, set updateCallback to callbackOptions. -
Otherwise, if callbackOptions is a
StartViewTransitionOptions
, then set updateCallback to callbackOptions’supdate
. -
If this’s active view transition is not null and its outbound post-capture steps is not null, then:
-
Let preSkippedTransition be a new
ViewTransition
in this’s relevant realm whose update callback is updateCallback.Note: The preSkippedTransition’s
types
are ignored here because the transition is never activated. -
Skip preSkippedTransition with an "
InvalidStateError
"DOMException
. -
Return preSkippedTransition.
Note: This ensures that a same-document transition that started after firing
pageswap
is skipped. -
-
Let viewTransition be the result of running the method steps for
startViewTransition(updateCallback)
given updateCallback. -
If callbackOptions is a
StartViewTransitionOptions
, set viewTransition’s active types to a clone oftypes
as a set. -
Return viewTransition.
9.1.2. activeViewTransition
Property
In order to provide ergonomic behavior, the active view transition is exposed to script via a document property.
activeViewTransition
, of type ViewTransition, readonly, nullable-
Returns the active view transition for the document, or null if there is no active view transition.
9.2. The ViewTransition
interface
[Exposed =Window ]interface {
ViewTransition readonly attribute Promise <undefined >;
updateCallbackDone readonly attribute Promise <undefined >;
ready readonly attribute Promise <undefined >;
finished undefined skipTransition ();attribute ViewTransitionTypeSet ; };
types
The ViewTransition
interface represents and controls a single same-document view transition,
i.e. a transition where the starting and ending document are the same,
possibly with changes to the document’s DOM structure.
viewTransition
.updateCallbackDone
-
A promise that fulfills when the promise returned by
updateCallback
fulfills, or rejects when it rejects.Note: The View Transition API wraps a DOM change and creates a visual transition. However, sometimes you don’t care about the success/failure of the transition animation, you just want to know if and when the DOM change happens.
updateCallbackDone
is for that use-case.) viewTransition
.ready
-
A promise that fulfills once the pseudo-elements for the transition are created, and the animation is about to start.
It rejects if the transition cannot begin. This can be due to misconfiguration, such as duplicate view-transition-names, or if
updateCallbackDone
returns a rejected promise.The point that
ready
fulfills is the ideal opportunity to animate the view transition pseudo-elements with the Web Animation API. viewTransition
.finished
-
A promise that fulfills once the end state is fully visible and interactive to the user.
It only rejects if
updateCallback
returns a rejected promise, as this indicates the end state wasn’t created.Otherwise, if a transition fails to begin, or is skipped (by
skipTransition()
), the end state is still reached, sofinished
fulfills. viewTransition
.skipTransition
()-
Immediately finish the transition, or prevent it starting.
This never prevents
updateCallback
being called, as the DOM change is independent of the transition. See § 1.5 Transitions as an enhancement for more details on this principle.If this is called before
ready
resolves,ready
will reject.If
finished
hasn’t resolved, it will fulfill or reject along withupdateCallbackDone
.
A ViewTransition
has the following:
- named elements
-
a map, whose keys are view transition names and whose values are captured elements. Initially a new map. Note: Since this is associated to the
ViewTransition
, it will be cleaned up when Clear view transition is called. - phase
-
One of the following ordered phases, initially "
pending-capture
":-
"
pending-capture
". -
"
update-callback-called
". -
"
animating
". -
"
done
".
Note: For the most part, a developer using this API does not need to worry about the different phases, since they progress automatically. It is, however, important to understand what steps happen in each of the phases: when the snapshots are captured, when pseudo-element DOM is created, etc. The description of the phases below tries to be as precise as possible, with an intent to provide an unambiguous set of steps for implementors to follow in order to produce a spec-compliant implementation.
-
- update callback
-
a
ViewTransitionUpdateCallback
or null. Initially null. - ready promise
-
a
Promise
. Initially a new promise in this’s relevant Realm. - update callback done promise
-
a
Promise
. Initially a new promise in this’s relevant Realm.Note: The ready promise and update callback done promise are immediately created, so rejections will cause
unhandledrejection
s unless they’re handled, even if the getters such asupdateCallbackDone
are not accessed. - finished promise
-
a
Promise
. Initially a new promise in this’s relevant Realm, marked as handled.Note: This is marked as handled to prevent duplicate
unhandledrejection
s, as this promise only ever rejects along with the update callback done promise. - transition root pseudo-element
-
a ::view-transition. Initially a new ::view-transition.
- initial snapshot containing block size
-
a tuple of two numbers (width and height), or null. Initially null.
Note: This is used to detect changes in the snapshot containing block size, which causes the transition to skip. Discussion of this behavior.
- active types
-
A
ViewTransitionTypeSet
, initially empty. - outbound post-capture steps
-
Null or a set of steps, initially null.
The finished
getter steps are to return this’s finished promise.
The ready
getter steps are to return this’s ready promise.
The updateCallbackDone
getter steps are to return this’s update callback done promise.
The types
getter steps are to return this’s active types.
9.2.1. The ViewTransitionTypeSet
Interface
[Exposed =Window ]interface {
ViewTransitionTypeSet setlike <DOMString >; };
The ViewTransitionTypeSet
object represents a set of strings, without special semantics.
Note: a ViewTransitionTypeSet
can contain strings that are invalid for :active-view-transition-type, e.g.
strings that are not a <custom-ident>.
9.2.2. skipTransition()
Method Steps
skipTransition()
are:
-
If this’s phase is not "
done
", then skip the view transition for this with an "AbortError
"DOMException
.
10. Determining view-transition-name automatically
10.1. Overview
This section is non-normative.
For an element to participate in a view transition, it needs to receive a unique view-transition-name. This can be tedious and verbose when multiple elements are involved in the same view transition, especially in cases where many of those are same-element transitions, as in, the element has the same view-transition-name in the old and new state.
To make this easier, setting the view-transition-name to auto would generate a view-transition-name for the element, or take it from the element’s id if present.
10.2. Examples
< ul > < li > Item 1</ li > < li > Item 2</ li > < li > Item 3</ li > < li > Item 4</ li > ...</ ul >
Ordinarily, each of these items would have to receive a unique view-transition-name:
li : nth-child ( 1 ) { view-transition-name : item1; } li:nth-child ( 2 ) { view-transition-name : item2; } li:nth-child ( 3 ) { view-transition-name : item3; } li:nth-child ( 4 ) { view-transition-name : item4; } ...
With auto or match-element, this CSS would work:
li{ view-transition-name : auto; /* or */ view-transition-name: match-element; }
The auto and match-element keywords have a minor difference, where auto would use the element’s id as the view-transition-name if it exists, making it potentially work across documents, while match-element only matches based on element identity.
10.3. Additions to view-transition-name
In addition to the existing values, the view-transition-name also accepts an auto keyword. To resolve the used value of view-transition-name for element:
-
Let computed be the computed value of view-transition-name.
-
If computed is none, return null.
-
If computed is a <custom-ident>, return computed.
-
Assert: computed is auto or match-element.
-
If computed is auto, element has an associated id, and computed is associated with the same root as element’s root, then return a unique string starting with "
-ua-
". Two elements with the same id must return the same string, regardless of their node document.Note: this means that a ::part() pseudo-element wouldn’t resolve to its matching element’s id.
-
Return a unique string starting with "
-ua-
". The string should remain consistent and unique for this element andDocument
, at least for the lifetime of element’s node document’s active view transition.Note: When used in a cross-document view transition, generated auto values never match, resulting in separate ::view-transition-group() pseudo-elements, one exiting and one entering.
A view-transition-name generated by auto is a tree-scoped name.
11. Nested view-transition groups
11.1. Overview
This section is non-normative.
By default, setting view-transition-name
on multiple elements generates a flat view transition tree, where all the ::view-transition-group() pseudo-elements are children of the ::view-transition pseudo-element.
This is sufficient for many simple use-cases, but there are some styling use-cases that cannot be achieved with a flat tree.
The most prominent use-case is clipping: with a flat tree, all pseudo-elements are clipped to the snapshot containing block, so clipping elements in the normal tree would lose their clipping during the view-transition, resulting in a broken visual effect.
The effects that have can have an unexpected visual effect in a flat tree:
-
Clipping (overflow, clip-path, border-radius): clipping affects the children of the element.
-
opacity, mask-image and filter: These effects that are designed to work on a fully rasterized image of a tree, rather than on each item individually.
-
3D transforms (transform-style, transform, perspective): to display the full range of 3D transform animations, some hierarchy needs to be kept.
To enable these use cases, this specification introduces the concept of nesting view-transition pseudo-elements. By using the view-transition-group CSS property, the author can assign a "parent group" for a generated ::view-transition-group() pseudo-element, creating a hierarchy in the view transition tree.
11.2. Examples
< section class = "container" > < article > Content</ article > </ section >
.container{ view-transition-name : container; } .container, ::view-transition-group-children ( container) { clip-path : circle (); } article{ view-transition-name : article; view-transition-group : container; }
The pseudo-element tree for this would be as follows:
::view-transition ├─ ::view-transition-group(container) │ ├─ ::view-transition-image-pair(container) │ | ├─ ::view-transition-old(container) │ | └─ ::view-transition-new(container) │ └─ ::view-transition-group-children(container) │ ├─ ::view-transition-group(article) │ | ├─ ::view-transition-old(article) │ | └─ ::view-transition-new(article) │ └─ ...other nested groups... └─ …other groups…
By applying the clip-path to both the containing element and its generated ::view-transition-group-children() pseudo-element, we preserve the clip during the transition, and by applying view-transition-group to the internal element referencing the container, we make the tree "nested" in a way that would apply this clipping.