This is a way to animate between views or pages. For example, animating a title that transitions into the next page based on the placement of the text - the old state is retained and the new state creates the transition.
/* default style */
:root::view-transition {
position: fixed;
inset: 0;
}
/* usage */
::view-transition {
/*styles go here*/
}
The main pseudo classes are ::view-*
and the following can be used for view transitions.
::view-transition
: the root of view transitions overlay (sits on the top of page content)
::view-transition-group
: single view transition snapshot group
::view-transition-image-pair
: a container for old
and new
view states
::view-transition-new
: snapshot of the state after the transition
::view-transition-old
: snap before the transition
::view-transition
ββ ::view-transition-group(root)
ββ ::view-transition-image-pair(root)
ββ ::view-transition-old(root)
ββ ::view-transition-new(root)
Firstly, the document.startViewTransition()
function needs to be triggered. The browser then takes a snapshot of the current DOM state, this includes positions and dimensions of all elements (occurring when startViewTransition()
is called). Then the tree illustrated above is created in a separate render layer, which sits above the document content. Whilst this callback is being made, the DOM changes are happening in the background but the user sees the old state.
document.startViewTransition(() => {
// DOM updates happen here
document.body.innerHTML = newContent;
return updatePromise; // Optional Promise that resolves when updates complete
});
Once the promise above resolves, the browser takes another snapshot to capture the new DOM state and assigns it to ::view-transition-new
. Now both the old and new state exist within the shadow DOM structure simultaneously. This is where the animation takes place. The old state is animated out and the new state is animated to come in (like a fade or a slide) and these animations run in parallel. The final part is to clean up the animation, this means removing the shadow DOM structure of all the pseudo-elements and the new DOM is revealed with the updated DOM.
Here is what happens to the shadow DOM within the browser:
<!-- This is created automatically and not directly visible in the DOM inspector -->
<div id="view-transition-container">
<!-- Root transition -->
<div class="::view-transition-group(root)">
<div class="::view-transition-image-pair(root)">
<div class="::view-transition-old(root)">
<!-- Screenshot of entire initial page -->
</div>
<div class="::view-transition-new(root)">
<!-- Screenshot of entire updated page -->
</div>
</div>
</div>
<!-- Product image specific transition -->
<div class="::view-transition-group(product-image)">
<div class="::view-transition-image-pair(product-image)">
<div class="::view-transition-old(product-image)">
<!-- Screenshot of product1.jpg -->
</div>
<div class="::view-transition-new(product-image)">
<!-- Screenshot of product2.jpg -->
</div>
</div>
</div>
</div>
In each ::view-transition-group
there is a new stack context, a container block for the descendants and an isolation boundary (so transitions donβt interfere with each other and maintain visual layering). The transition layer is placed above the document content but below the browser UI elements like the URL. Named elements with higher source appear above those with lower source order. If you want to move to a different parent, change the size, position or appearance, you do not have to change elements within the DOM, view transitions does this for you as along as the same transition name is used for both states.
Stable in most browsers other than Safari.
View transitions is supported with Astro and can be used via astro:transition
. Currently on this blog, it is used to go between the main page and the category pages.
Hereβs how Iβve implemented it:
Categories.astro
---
import { ViewTransitions } from "astro:transitions";
---
<html lang="en">
<head>
<ViewTransitions />
<BaseHead title={title} description={description} />
<style>
...
<style/>
</head>
<body>
<div class="container">
<div class="side-menu"><Header /></div>
<div class="blog-list-section"><section><slot /></section></div>
<footer class="footer-section"><Footer /></footer>
</div>
</body>
</html>
Home.astro
---
import { ViewTransitions } from "astro:transitions";
---
<html lang="en">
<head>
<ViewTransitions />
<BaseHead title={""} description={""} />
<style>
...
<style/>
</head>
<div class="container">
<div class="side-menu"><Header /></div>
<div class="header-section"><SubHeader /></div>
<div class="blog-list-section"><section><slot /></section></div>
<footer class="footer-section"><Footer /></footer>
</div>
</html>
This is an experimental feature at the time of writing this post.
import { unstable_ViewTransition as ViewTransition } from "react";
<ViewTransition>
<div>...</div>
</ViewTransition>;
All of these props are currently optional.
enter
: <ViewTransition enter="slide-in">
::view-transition-group(.slide-in) {
}
::view-transition-old(.slide-in) {
}
::view-transition-new(.slide-in) {
}
exit
update
share
default
name
Again, all optional
onEnter
onExit
onShare
onUpdate
function Child() {
return <ViewTransition>Hi</ViewTransition>;
}
function Parent() {
const [show, setShow] = useState();
if (show) {
return <Child />;
}
return null;
}