- Introduction.
- Why do we need a new API for this? - exploring the difficulties of achieving page transitions with existing APIs.
- MPA vs SPA solutions - how this API covers both same-document and cross-document transitions.
- Revisiting the cross-fade example - how to perform a cross-fade with this API.
- How the cross-fade worked - exploring the behind-the-scenes detail of the cross-fade.
- Simple customization - changing the default cross-fade.
- Transitioning multiple elements - moving parts of the page independently.
- Transitioning elements don't need to be the same DOM element - creating a transition where a thumbnail 'grows' into the main content.
- Transitioning elements don't need to exist in both states.
- Customizing the transition based on the type of navigation - e.g. creating 'reverse' transitions for 'back' traversals.
- Animating with JavaScript - because some transitions aren't possible with CSS alone.
- Cross-document same-origin transitions - MPA page transitions.
- Compatibility with existing developer tooling.
- Compatibility with frameworks.
- Error handling - Ensuring DOM changes don't get lost, or stuck.
- Handling ink overflow - Dealing with things like box shadows.
- Full default styles & animation.
- Future work.
- Nested transition Groups - cases where the 'flattened' model isn't the best model.
- More granular style capture - cases where images aren't enough.
- Better pseudo-element selectors - because right now they kinda suck.
- Transitions targeted to a specific element - transitions that aren't the whole 'page'.
- Security/Privacy considerations.
- Interactivity and accessibility.
Smooth page transitions can lower the cognitive load by helping users stay in context as they navigate from Page-A to Page-B, and reduce the perceived latency of loading.
hgnJfPFUbGlucFegEEtl.mp4
Typically, navigations on the web involve one document switching to another. Browsers try to eliminate an intermediate flash-of-white, but the switch between views is still sudden and abrupt. Until View Transitions, there was nothing developers could do about that without switching to an SPA model. This feature provides a way to create an animated transition between two documents, without creating an overlap between the lifetime of each document.
Although switching to an SPA allows developers to create transitions using existing technologies, such as CSS transitions, CSS animations, and the Web Animation API, it's something most developers and frameworks avoid, or only do in a limited fashion, because it's harder than it sounds.
Let's take one of the simplest transitions: a block of content that cross-fades between states.
To make this work, you need to have a phase where both the old and new content exist in the document at the same time. The old and new content will need to be in their correct viewport positions, which usually means they'll be overlaying each other, while maintaining layout with the rest of the page, so you'll probably need some form of wrapper to manage that. Another reason for the wrapper is to allow the two elements to correctly cross-fade using mix-blend-mode: plus-lighter
. Then, the old content will fade from opacity: 1
to opacity: 0
, while the new content fades from opacity: 0
to opacity: 1
. Once that's complete, the old content is removed, perhaps along with some of the wrapper(s) that were used just for the transition.
However, there are a number of accessibility and usability pitfalls in this simple example. The phase where both contents exist at the same time creates an opportunity for users of assistive technology to get confused between the two. The transition is a visual affordance, it shouldn't be seen by things like screen readers. There's also an opportunity for the user to interact with the old content in a way the developer didn't prevent (e.g. pressing buttons). The second DOM change after the transition, where the old content is removed, can create more accessibility issues, as the DOM mutation can cause an additional aria-live announcement of the same content. It's also a common place for focus state to get confused, particularly in frameworks where the new content DOM used in the transition may not be the same DOM used in the final state (depending on how virtual DOMs are diffed, it may not realize it's the same content, particularly if containers have changed).
If the content is large, such as the main content, the developer has to handle differences in the root scroll position between the two states. At the very least, one of the pieces of content will need to be offset to counteract the scroll difference between the two, and unset once the transition is complete.
And this is just a simple cross-fade. Things get an order of magnitude more complicated when page components need to transition position between the states. Folks have created large complex plugins, built on top of even larger libraries, just to handle this small part of the problem. Even then, they don't handle cases where the element gets clipped by some parent, via overflow: hidden
or similar. To overcome this, developers tend to pop the animating element out to the <body>
so it can animate freely. To achieve that, the developer needs to alter their CSS so the element looks the same as a child of <body>
as it does in its final place in the DOM. This discourages developers from using the cascade, and it plays badly with contextual styling features such as container queries.
If your site is an SPA, none of this is impossible, it's just really hard. With regular navigations (sometimes referred to as Multi-Page Apps, or MPAs), it is impossible.
The View Transitions feature follows the trend of transition APIs on platforms like Android, iOS/Mac and Windows, by allowing developers to continue to update page state atomically (either through DOM changes or cross-document navigations), while defining highly tailored transitions between the two states.
The current spec and experimental implementation in Chrome (behind the chrome://flags/#document-transition
flag) focuses on SPA transitions. However, the model has also been designed to work with cross-document navigations. The specifics for cross-document navigations are covered later in this document.
This doesn't mean we consider the MPA solution less important. In fact, developers have made it clear that it's more important. We have focused on SPAs due to the ease of prototyping, so those APIs have had more development. However, the overall model has been designed to work for MPAs, with a slightly different API around it.
As described above, creating a cross-fade transition using existing platform features is more difficult than it sounds. Here's how to do it with View Transitions:
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
document.startViewTransition(() => updateTheDOMSomehow(data));
}
(the API is described in detail in the next section)
And now there's a cross-fade between the states:
9rdbsCmBXKOYxOQNjBMI.mp4
Ok, a cross-fade isn't that impressive. Thankfully, transitions can be customized, but before we get to that, here's how this basic cross-fade worked:
Taking the code sample from above:
document.startViewTransition(() => updateTheDOMSomehow(data));
When document.startViewTransition()
is called, the API captures the current state of the page. This includes taking a screenshot, which is async as it happens in the render steps of the event loop.
Once that's complete, the callback passed to document.startViewTransition()
is called. That's where the developer changes the DOM.
Rendering is paused while this happens, so the user doesn't see a flash of the new content. Although, the render-pausing has an aggressive timeout.
Once the DOM is changed, the API captures the new state of the page, and constructs a pseudo-element tree like this:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
(the specific function of each part of this tree, and their default styles, is covered later in this document)
The ::view-transition
sits in a top-layer, over everything else on the page.
::view-transition-old(root)
is a screenshot of the old state, and ::view-transition-new(root)
is a live representation of the new state. Both render as CSS replaced content.
The old image animates from opacity: 1
to opacity: 0
, while the new image animates from opacity: 0
to opacity: 1
, creating a cross-fade.
Once the animation is complete, the ::view-transition
is removed, revealing the final state underneath.
Behind the scenes, the DOM just changed, so there isn't a time where both the old and new content existed at the same time, avoiding the accessibility, usability, and layout issues.
The animation is performed using CSS animations, so it can be customized with CSS.
All of the pseudo-elements above can be targeted with CSS, and since the animations are defined using CSS, you can modify them using existing CSS animation properties. For example:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
With that one change, the fade is now really slow:
90h6Ppxza6oPqNiMpTPE.mp4
Or, more practically, here's an implementation of Material Design's shared axis transition:
@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;
}
And the result:
BRT5dMgEzpixRmrVYKwN.mp4
Note: In this example, the animation always moves from right to left, which doesn't feel natural when clicking the back button. How to change the animation depending on the direction of navigation is covered later in the document.
In the previous demo, the whole page is involved in the shared axis transition. But that doesn't seem quite right for the heading, as it slides out just to slide back in again.
To solve this, View Transitions allow you to extract parts of the page to animate independently, by assigning them a view-transition-name
:
.header {
view-transition-name: header;
contain: layout;
}
.header-text {
view-transition-name: header-text;
contain: layout;
}
Independently transitioning elements needs to have layout
or paint
containment, and avoid fragmentation, so the element can be captured as a single unit.
The page will now be captured in three parts: The header, the header text, and the remaining page (known as the 'root').
ScreenFlow.mp4
This results in the following pseudo-element tree for the transition:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
│
├─ ::view-transition-group(header)
│ └─ ::view-transition-image-pair(header)
│ ├─ ::view-transition-old(header)
│ └─ ::view-transition-new(header)
│
└─ ::view-transition-group(header-text)
└─ ::view-transition-image-pair(header-text)
├─ ::view-transition-old(header-text)
└─ ::view-transition-new(header-text)
The new pseudo-elements follow the same pattern as the first, but for a subset of the page. For instance, ::view-transition-old(header-text)
is a 'screenshot' of the header text, and ::view-transition-new(header-text)
is a live representation of the new header text. Although, in this case, the header text images are identical, but the element has changed position.
Without any further customization, here's the result:
eXu6vohojllPLNEQScjO.mp4
Note how the top header remains static.
As well as the cross-fade between the old-image and the new-image, another default animation transforms the ::view-transition-group
from its before position to its after position, while also transitioning its width and height between the states. This causes the heading text to shift position between the states. Again, the developer can use CSS to customize this as they wish.
The high-level purpose of each pseudo-element:
::view-transition-group
- animates size and position between the two states.::view-transition-image-pair
- provides blending isolation, so the two images can correctly cross-fade.::view-transition-old
and::view-transition-new
- the visual states to cross-fade.
The full default styles and animations of the pseudo-elements are covered later in the document.
In the previous examples, view-transition-name
was used to create separate transition elements for the header, and the text in the header. These are conceptually the same element before and after the DOM change, but you can create transitions where that isn't the case.
For instance, the main video embed can be given a view-transition-name
:
.full-embed {
view-transition-name: full-embed;
contain: layout;
}
Then, when the thumbnail is clicked, it can be given the same view-transition-name
, just for the duration of the transition:
thumbnail.onclick = () => {
thumbnail.style.viewTransitionName = "full-embed";
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = "";
updateTheDOMSomehow();
});
};
And the result:
283vqtaDXSaGRTn5nEEn.mp4
The thumbnail now transitions into the main image. Even though they're conceptually (and literally) different elements, the transition API treats them as the same thing because they shared the same view-transition-name
.
This is useful for cases like above where one element is 'turning into' another, but also for cases where a framework creates a new Element
for something even though it hasn't really changed, due to a virtual DOM diffing mismatch.
Also, this model is essential for MPA navigations, where all elements across the state-change will be different DOM elements.
It's valid for some transition elements to only exist on one side of the DOM change, such as a side-bar that doesn't exist on the old page, but exists in the new page.
For example, if an element only exists in the 'after' state, then it won't have a ::view-transition-old
, and its ::view-transition-group
won't animate by default, it'll start in its final position.
In some cases, the elements captured, and the resulting animations, should be different depending on the source & target page, and also different depending on the direction of navigation.
hgnJfPFUbGlucFegEEtl.mp4
In this example, the transition between the thumbnails page and the video page is significantly different to the transition between video pages. Also, animation directions are reversed when navigating back.
There isn't a specific feature for handling this. Developers can add class names to the document element, allowing them to write selectors that change which elements get a view-transition-name
, and which animations should be used.
In particular, the Navigation API makes it easy to distinguish between a back vs forward traversal/navigation.
The ready
promise on ViewTransition
returned by document.startViewTransition()
fulfills when both states have been captured and the pseudo-element tree has been successfully built. This provides developers with a point where they can animate those pseudo-elements with the Web Animation API.
For example, if the developer wanted to create a circular-reveal animation from the point of the last click:
let lastClick;
addEventListener("click", (event) => (lastClick = event));
async function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
const transition = document.startViewTransition(() => {
// 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.sqrt(
Math.max(x, innerWidth - x) ** 2 + Math.max(y, innerHeight - y) ** 2
);
updateTheDOMSomehow(data);
});
animateTransition(transition);
// spaNavigate should resolve when the DOM updates,
// not when the transition finishes.
return transition.updateCallbackDone;
}
async function animateTransition(transition) {
await transition.ready;
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)",
}
);
return transition.finished;
}
And here's the result:
MrrqwatxWSPdobfDR1Qo.mp4
This section outlines the navigation specific aspects of the ViewTransition API. The rendering model for generating snapshots and displaying them using a tree of targetable pseudo-elements is the same for both SPA/MPA.
The first step is to add a new meta tag to the old and new Documents. This tag indicates that the author wants to enable transitions for same-origin navigations to/from this Document.
<meta name="view-transition" content="same-origin">
The above is equivalent to the browser implicitly executing the following script in the SPA API:
document.startViewTransition(() => updateDOMToNewPage());
This results in a cross-fade between the 2 Documents from the default CSS set up by the browser. The transition executes only if this tag is present on both the old and new Documents. The tag must also be added before the body element is parsed.
The motivation for a declarative opt-in, instead of a script event, is:
-
Enabling authors to define transitions with no script. If the transition doesn't need to be customized based on the old/new URL, it can be defined completely in CSS.
-
Avoiding undue latency in the critical path for browser initiated navigations like back/forward. We want to avoid dispatch of a script event for each of these navigations.
Issue: The meta tag can be used to opt-in to other navigation types going forward: same-document, same-site, etc.
Issue: This prevents the declaration being controlled by media queries, which feels important for prefers-reduced-motion
.
Script can be used to customize a transition based on the URL of the old/new Document; or the current state of the Document when the transition is initiated. The Document could've been updated since first lold from user interaction.
document.addEventListener("crossdocumentviewtransitionoldcapture", (event) => {
// Cancel the transition (based on new URL) if needed.
if (shouldNotTransition(event.toURL)) {
event.preventDefault();
return;
}
// Set up names on elements based on the new URL.
if (shouldTagThumbnail(event.toURL)) {
thumbnail.style.viewTransitionName = "full-embed";
}
// Add opaque contextual information to share with the new Document.
// This must be [serializable object](https://developer.mozilla.org/en-US/docs/Glossary/Serializable_object).
event.setInfo(createTransitionInfo(event.toURL));
});
// This event must be registered before the `body` element is parsed.
document.addEventListener("crossdocumentviewtransition", (event) => {
// Cancel the transition (based on old URL) if needed.
if (shouldNotTransition(event.fromURL)) {
event.preventDefault();
return;
}
// The `ViewTransitionNavigation` object associated with this transition.
const transition = event.transition;
// Retrieve the context provided by the old Document.
const info = event.info;
// Add render-blocking resources to delay the first paint and transition
// start. This can be customized based on the old Document state when the
// transition was initiated.
markRenderBlockingResources(info);
// The `ready` promise resolves when the pseudo-elements have been generated
// and can be used to customize animations via script.
transition.ready.then(() => {
document.documentElement.animate(...,
{
// Specify which pseudo-element to animate
pseudoElement: "::view-transition-new(root)",
}
);
// Remove viewTransitionNames tied to this transition.
thumbnail.style.viewTransitionName = "none";
});
// The `finished` promise resolves when all animations for the transition are
// finished or cancelled and the pseudo-elements have been removed.
transition.finished.then(() => { ... });
});
This provides the same scripting points as the SPA API, allowing developers to set class names to tailor the animation to a particular type of navigation.
Issue: Event names are verbose. Bikeshedding needed.
Issue: Do we need better timing for crossdocumentviewtransition
event? Especially for Documents restored from BFCache.
Issue: Customizing which resources are render-blocking in crossdocumentviewtransition
requires it to be dispatched before parsing body
, or explicitly allow render-blocking resources to be added until this event is dispatched.
Issue: We'd likely need an API for the developer to control how much Document needs to be fetched/parsed before the transition starts.
Issue: The browser defers painting the new Document until all render-blocked resources have been fetched or timed out. Do we need an explicit hook for when this is done or could the developer rely on existing load
events to detect this? This would allow authors to add viewTransitionNames based on what the new Document's first paint would look like.
Issue: Since crossdocumentviewtransitionoldcapture
is dispatched after redirects and only if the final URL is same-origin, it allows the current Document to know whether the navigation eventually ended up on a cross-origin page. This likely doesn't matter since the site could know this after the navigation anyway but knowing on the current page before the navigation commits is new.
Since this feature is built on existing concepts such as pseudo-elements and CSS animations, tooling for this feature should fit in with existing developer tooling.
In Chrome's experimental implementation, the pre-existing animation panel can be used to debug transitions, and the pseudo-elements are exposed in the elements panel.
DMH7qPqMszyVbTYOA2zd.mp4
The DOM update can be async, to cater for frameworks that queue state updates behind microtasks. This is signaled by returning a promise from the document.startViewTransition()
callback, which is easily achieved with an async function:
document.startViewTransition(async () => {
await updateTheDOMSomehow();
});
However, the pattern above assumes the developer is in charge of DOM updates, which isn't the case with most web frameworks. To assess the compatibility of this API with frameworks, the demo site featured in this explainer was built using Preact, and uses a React-style hook to wrap the above API and make it usable with React/Preact.
As long as the framework provides a notification when the DOM is updated, which they already do to allow custom handling of elements, the transition API can be made to work with the framework.
This feature is built with the view that a transition is an enhancement to a DOM change. For example:
document.startViewTransition(async () => {
await updateTheDOMSomehow();
});
The API could discover an error before calling the document.startViewTransition()
callback, meaning the transition cannot happen. For example, it may discover two elements with the same view-transition-name
, or one of the transition elements is fragmented in a way that's incompatible with the API. In this case we still call the document.startViewTransition()
callback, because the DOM change is more important than the transition, and being unable to create a transition is not a reason to prevent the DOM change.
However, if a transition cannot be created, the ready promise on the returned
ViewTransition` will reject.
Error detection is also the reason why document.startViewTransition()
takes a callback, rather than a model where the developer calls a method to signal when the DOM is changed:
// Not the real API, just an alternative example:
const transition = new ViewTransition();
await transition.prepare();
await updateTheDOMSomehow();
transition.ready();
In a model like the one above, if updateTheDOMSomehow()
throws, transition.ready
would never be called, so the API would be in a state where it doesn't know if DOM change failed, or if it's just taking a long time. The callback pattern avoids this gotcha – we get to see the thrown error, and abandon the transition quickly.
The Navigation API and Web Locks API use this same pattern for the same reason.
Elements can paint outside of their border-box for a number of reasons, such as box-shadow
.
The ::view-transition-old
and ::view-transition-new
will be the border box size of the original element, but the full ink overflow will be included in the image. This is achieved via object-view-box
, which allows replaced elements to paint outside their bounds.
The ::view-transition-group
animates its width
and height
by default, which usually means the animations will run on the main thread.
However, width
and height
was deliberately chosen for developer convenience, as it plays well with things like object-fit
and object-position
.
gXiaS9IpE70fnv4kkrK5.mp4
In this example, a 4:3 thumbnail transitions into a 16:9 main image. This is relatively easy with object-fit
, but would be complex using only transforms.
Due to the simple nature of these pseudo-element trees, these animations should be able to run off the main thread. However, if the developer adds something that requires layout, such as a border, the animation will fall back to main thread.
Default styles:
::view-transition {
// Aligns this element with the "snapshot viewport". This is the viewport when all retractable
// UI (like URL bar, root scrollbar, virtual keyboard) are hidden.
position: fixed;
top: -10px;
left: -15px;
}
Default styles:
::view-transition-group(*) {
/*= Styles for every instance =*/
position: absolute;
top: 0px;
left: 0px;
will-change: transform;
pointer-events: auto;
/*= Styles generated per instance =*/
/* Dimensions of the new element */
width: 665px;
height: 54px;
/* A transform that places it in the viewport position of the new element. */
transform: matrix(1, 0, 0, 1, 0, 0);
writing-mode: horizontal-tb;
animation: 0.25s ease 0s 1 normal both running
page-transition-group-anim-main-header;
}
Default animation:
@keyframes page-transition-group-anim-main-header {
from {
/* Dimensions of the old element */
width: 600px;
height: 40px;
/* A transform that places it in the viewport position of the old element. */
transform: matrix(2, 0, 0, 2, 0, 0);
}
}
Default styles:
::view-transition-image-pair(*) {
/*= Styles for every instance =*/
position: absolute;
inset: 0px;
/*= Styles generated per instance =*/
/* Set if there's an old and new image, to aid with cross-fading.
This is done conditionally as isolation has a performance cost. */
isolation: isolate;
}
Default animation: none.
This is a replaced element displaying the capture of the old element, with a natural aspect ratio of the old element.
::view-transition-old(*) {
/*= Styles for every instance =*/
position: absolute;
inset-block-start: 0px;
inline-size: 100%;
block-size: auto;
will-change: opacity;
/*= Styles generated per instance =*/
/* Set if there's an old and new image, to aid with cross-fading.
This is done conditionally as isolation has a performance cost. */
mix-blend-mode: plus-lighter;
/* Allows the image to be the layout size of the element,
but allow overflow (to accommodate ink-overflow)
and underflow (cropping to save memory) in the image data. */
object-view-box: inset(0);
animation: 0.25s ease 0s 1 normal both running blink-page-transition-fade-out;
}
Note that the block-size
of this element is auto, so it won't stretch the image as the container changes height. The developer can change this if they wish.
Default animation:
@keyframes page-transition-fade-out {
from {
opacity: 0;
}
}
@keyframes page-transition-fade-in {
to {
opacity: 0;
}
}
There are parts to this feature that we're actively thinking about, but aren't fully designed.
In the current design, each ::view-transition-group
is a child of the ::view-transition
. This works really well in most cases, but not all:
ScreenFlow.mp4
The element moving from one container to the other benefits from the flat arrangement of ::view-transition-group
s, as it doesn't get clipped by the parent. However, the elements that remain in the container do benefit from the clipping provided by the parent.
The rough plan is to allow nesting via an opt-in (all API names used here are for entertainment purposes only):
.container {
view-transition-name: container;
contain: paint;
}
.child-item {
view-transition-name: child-item;
contain: layout;
page-transition-style-or-whatever: nested;
}
With this opt in, rather than the containers being siblings:
::view-transition
├─ …
├─ ::view-transition-group(container)
│ └─ ::view-transition-image-pair(container)
│ └─ …
└─ ::view-transition-group(child-item)
└─ ::view-transition-image-pair(child-item)
└─ …
…the child-item
would be nested in its closest parent that's also a transition element:
::view-transition
├─ …
└─ ::view-transition-group(container)
├─ ::view-transition-image-pair(container)
│ └─ …
└─ ::view-transition-group(child-item)
└─ ::view-transition-image-pair(child-item)
└─ …
By default, elements are captured as images. This means if a rounded box is transitioning into a different size box with the same border-radius, there'll be some imperfect scaling of the corners during the transition.
This often isn't as bad as it sounds in practice, particularly in fast transitions. And, developers can build custom animations with clip-paths to work around the issue in some cases. However, we are considering a different opt-in capture mode, where the computed styles of the transition elements are captured, allowing for transitions that involve layout.
In this mode, the content of the element would still be an image, but the element itself would have things like the border-radius
and box-shadow
copied over, rather than being baked into an image.
However, since these animations would involve layout, they would need to run on the main thread.
This feature makes use of nested pseudo-elements. It isn't the first feature to do that, as there's also ::before::marker
, but this feature has more than two levels of nesting.
Right now, all pseudo elements are accessed from the root element, which doesn't really express their nesting. However, if the nesting was fully expressed, you'd end up with selectors like:
::view-transition-group(foo)::image-wrapper::old-image {
/* … */
}
We have proposed a new combinator to make it easier to select descendant pseudo elements w3c/csswg-drafts#7346.
::view-transition-group(foo) :>> old-image {
/* … */
}
This will play well with CSS nesting:
::view-transition-group(foo) {
& :>> old-image {
/* … */
}
& :>> new-image {
/* … */
}
}
In the current design, the transition acts across the whole document. However, developers have expressed interest in using this system, but limited to a single element. For example, allowing two independent components to perform transitions.
This is being discussed in WICG#52 and a rough proposal is here.
The security considerations below cover same-origin transitions.
- Script can never read pixel content in the images. This is necessary since the document may embed cross-origin content (iframes, CORS resources, etc.) and multiple restricted user information (visited links history, dictionary used for spell check, etc.)
- If an element is captured as a 'computed style + content image', any external resources specified on the container, such as background images, will be re-fetched in the context of the new page to account for differences in sandboxing.
Cross-origin transitions aren't yet defined, but are likely to be heavily restricted.
- Page transitions are a purely visual affordance. In terms of interactivity, transition elements will behave like
div
s regardless of the original element. Developers could break this intent by adding interactivity directly to the transition element, e.g. by deliberately adding atabindex
attribute. But this isn't recommended. - The page transition stage will be hidden from assistive technologies such as screen readers.
- The duration for which DOM rendering is suppressed, to allow an author to asynchronously switch to the new DOM, input processing is also paused. This is necessary since the visual state presented to the user is inconsistent with the DOM state used for hit-testing.