| name | web-view-transition |
| description | Expert guidance for implementing the View Transition API — covering same-document (SPA) transitions with `document.startViewTransition()`, cross-document (MPA) transitions with `@view-transition`, customizing animations via CSS pseudo-elements (`::view-transition-old`, `::view-transition-new`, `::view-transition-group`), per-element animations with `view-transition-name`, JavaScript control via the `ViewTransition` promises (`ready`, `finished`, `updateCallbackDone`), context-aware transition types with `:active-view-transition-type()`, and graceful fallbacks for unsupported browsers. Use this skill when someone wants page transition animations, shared-element transitions, slide/fade/circular-reveal effects, or asks about `startViewTransition`, `@view-transition`, `view-transition-name`, `::view-transition-*` pseudo-elements, or the `ViewTransition` object — even if they just say "smooth page transitions" or "animate between routes".
|
| license | MIT |
| metadata | {"author":"Ikuma Yamashita","version":"1.0.0"} |
Web View Transition API
The View Transition API creates animated transitions between DOM states or page
navigations with minimal code. The browser handles snapshotting old/new states
and animating between them.
Browser support: Chrome 111+, Edge 111+, Firefox 144+, Safari 18+
(Baseline 2025). Always provide a no-animation fallback.
Quick mental model
When a transition fires, the browser:
- Captures a static snapshot of the current view
- Applies your DOM change
- Captures a live snapshot of the new view
- Animates old → new using a pseudo-element tree layered above the page
The pseudo-element tree looks like:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root) ← static old snapshot
└─ ::view-transition-new(root) ← live new snapshot
Named elements get their own group alongside root.
SPA (same-document) transitions
Basic usage
Wrap your DOM update in document.startViewTransition(). The default
cross-fade animation applies automatically.
function navigate(updateFn) {
if (!document.startViewTransition) {
updateFn();
return;
}
document.startViewTransition(updateFn);
}
The callback can be async; the transition waits for the returned promise.
document.startViewTransition(async () => {
const data = await fetchNewContent();
renderContent(data);
});
Animate specific elements separately
By default the whole page cross-fades. Assign view-transition-name to
elements you want to animate independently. Names must be unique per frame.
.hero-image {
view-transition-name: hero;
}
nav {
view-transition-name: site-nav;
}
This generates separate ::view-transition-group(hero) and
::view-transition-group(site-nav) branches. The browser automatically
morphs position and size between old/new states for those elements.
Avoid conflicts: if two rendered elements share the same name, ready
rejects and the transition is skipped. Assign names dynamically and clean them
up:
document.startViewTransition(() => {
clickedCard.style.viewTransitionName = "selected-card";
updateDOM();
requestAnimationFrame(() => {
clickedCard.style.viewTransitionName = "";
});
});
MPA (cross-document) transitions
No JavaScript needed. Both the outgoing and destination pages must opt in with
the CSS @view-transition at-rule. Both pages must be same-origin.
@view-transition {
navigation: auto;
}
That's it for a default cross-fade. Customize animations in the destination
page's CSS.
Customizing animations
Target the pseudo-elements to override the default cross-fade. Prefer targeting
::view-transition-group() for duration/easing so the values cascade to
old/new consistently.
::view-transition-group(root) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
@keyframes slide-out {
to {
transform: translateY(-100%);
}
}
@keyframes slide-in {
from {
transform: translateY(100%);
}
}
::view-transition-old(root) {
animation: 0.4s ease-in both slide-out;
}
::view-transition-new(root) {
animation: 0.4s ease-in both slide-in;
}
Use * to target all named groups at once:
::view-transition-group(*) {
animation-duration: 0.3s;
}
Circular-reveal (Web Animations API)
For programmatic animations that depend on runtime data (e.g. click position),
use the ViewTransition.ready promise together with the Web Animations API.
let lastClick;
document.addEventListener("click", (e) => (lastClick = e));
function navigate(updateFn) {
if (!document.startViewTransition) {
updateFn();
return;
}
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
const r = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
const transition = document.startViewTransition(updateFn);
transition.ready.then(() => {
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${r}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: "ease-in",
pseudoElement: "::view-transition-new(root)",
},
);
});
}
ViewTransition object (JavaScript control)
document.startViewTransition() returns a ViewTransition object. Access it
in MPA transitions via PageSwapEvent.viewTransition (outgoing page) and
PageRevealEvent.viewTransition (incoming page). Also available anywhere via
document.activeViewTransition.
| Promise | Fulfills when |
|---|
updateCallbackDone | The DOM update callback resolved |
ready | Pseudo-elements created, animation about to start |
finished | Animation completed, new view is interactive |
const transition = document.startViewTransition(updateFn);
transition.updateCallbackDone.then(() => console.log("DOM updated"));
transition.ready.then(() => {
});
transition.finished.then(() => {
element.style.viewTransitionName = "";
});
transition.skipTransition();
Back/forward direction pattern
async function handleNav(isBack) {
if (isBack) document.documentElement.classList.add("back-nav");
const transition = document.startViewTransition(updateFn);
try {
await transition.finished;
} finally {
document.documentElement.classList.remove("back-nav");
}
}
.back-nav::view-transition-old(root) {
animation-name: slide-out-right;
}
.back-nav::view-transition-new(root) {
animation-name: slide-in-left;
}
Transition types (context-aware animations)
Types let you apply different animations to the same elements depending on
context (e.g. "forwards" vs "backwards" in a gallery).
SPA — pass types to startViewTransition
document.startViewTransition({
update() {
renderNextImage();
},
types: ["forwards"],
});
Modify types dynamically on the returned object:
const vt = document.startViewTransition({ update: renderFn });
if (isBack) vt.types.add("backwards");
MPA — set types in @view-transition
@view-transition {
navigation: auto;
types: chapter-forward;
}
Or assign dynamically via pageswap/pagereveal events:
window.addEventListener("pageswap", (e) => {
if (e.viewTransition && goingForward(e)) {
e.viewTransition.types.add("forwards");
}
});
Apply CSS per type
html:active-view-transition {
:root {
view-transition-name: none;
}
.card {
view-transition-name: card;
}
}
html:active-view-transition-type(forwards) {
&::view-transition-old(card) {
animation-name: slide-out-left;
}
&::view-transition-new(card) {
animation-name: slide-in-right;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(card) {
animation-name: slide-out-right;
}
&::view-transition-new(card) {
animation-name: slide-in-left;
}
}
MPA: pageswap / pagereveal pattern
Use these events to set view-transition-name dynamically on MPA pages,
enabling shared-element transitions between specific elements.
window.addEventListener("pageswap", async (e) => {
if (!e.viewTransition) return;
const targetUrl = new URL(e.activation.entry.url);
if (isDetailPage(targetUrl)) {
const id = extractId(targetUrl);
document.querySelector(`#item-${id} img`).style.viewTransitionName =
"hero-img";
await e.viewTransition.finished;
document.querySelector(`#item-${id} img`).style.viewTransitionName = "";
}
});
window.addEventListener("pagereveal", async (e) => {
if (!e.viewTransition) return;
const fromUrl = navigation.activation.from?.url;
if (fromUrl && isListPage(new URL(fromUrl))) {
document.querySelector(".detail-hero").style.viewTransitionName =
"hero-img";
await e.viewTransition.finished;
document.querySelector(".detail-hero").style.viewTransitionName = "";
}
});
Common pitfalls
Duplicate view-transition-name: if two visible elements share a name,
ViewTransition.ready rejects and the transition is skipped silently. Clean up
names after transitions using finished.
bfcache conflicts: when the back button is pressed, the page is restored
from cache. If names were left set, the next pagereveal handler sets them
again → duplicate → skipped. Always remove names after finished.
Hidden page skips transition: if the page is not visible (minimized, other
tab), startViewTransition skips the animation automatically. This is correct
behavior — the DOM update still runs.
MPA cross-origin: cross-document transitions only work between same-origin
pages. A cross-origin redirect in the chain also disables the activation
property (PageSwapEvent.activation returns null).
match-element keyword: you can use view-transition-name: match-element
to automatically assign unique names to all matched elements, useful for list
items without manual ID assignment.
Accessibility
Respect the user's motion preference. When prefers-reduced-motion: reduce is
set, either remove the animation or make it instant.
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}
Reference files
Read these when you need deeper detail than what is covered above.
references/api.md — Complete API reference: browser support table,
Document.startViewTransition() signature, all ViewTransition promises,
ViewTransitionTypeSet methods, pageswap/pagereveal event objects, and
every CSS addition (@view-transition, view-transition-name,
view-transition-class, pseudo-classes, pseudo-elements, <link rel="expect">)
references/using.md — Detailed walkthrough: how the transition process
works step-by-step, SPA and MPA setup, CSS animation customization, per-element
naming, the circular-reveal Web Animations API pattern, back/forward direction
patterns, pageswap/pagereveal shared-element examples, and MPA render
stabilization with <link rel="expect">
references/using-types.md — In-depth guide to transition types: SPA
types option, ViewTransition.types.add(), type-specific CSS with
:active-view-transition-type(), static MPA types in @view-transition,
and dynamic MPA types via pageswap/pagereveal with direction detection