On this page
Cross-Document View Transitions: Lifecycle & Pipeline
Understand how cross-document View Transitions coordinate navigation lifecycle, snapshot capture, BFCache, and browser rendering pipelines.
Reference Card
- Specification
- CSS [View Transitions](/blog/view-transitions/) Module Level 2 (W3C)
- Status
- Editor's Draft - not yet Candidate Recommendation
- Last revision
- 13 July 2026
- Editors
- Noam Rosenthal, Khushal Sagar, Vladimir Levin,
- Tab Atkins-Bittner (Google)
- Chrome / Edge 126+ (June 2024)
- Safari 18.2+ (December 2024)
- Firefox Not supported as of this writing
- CSS View Transitions Module Level 1, HTML Standard,
- Navigation API, DOM Standard
- pageswap / pagereveal events · @view-transition rule
- Capture the old/new state · Render-blocking stabilization
- Pseudo-element tree generation
- WebKit omits the fragmented-content capture check present in Blink
- Firefox has not shipped cross-document support
- Two-phase capture timing is Chromium-prototype-only, not normative
- view-transition-name persisting into a BFCache snapshot
- Duplicate names aborting the transition silently
- Overusing the render-blocking mechanism, freezing the old state
Note. This is a Working Draft-stage specification under active revision. Cross-document view transitions shipped in Chromium (126) and Safari (18.2) as of the versions above; Firefox has not shipped them. Several normative details in this reference could not be independently verified against the complete formal algorithm text - this is disclosed explicitly in Section 8 rather than reconstructed.
Reading path. If you arrived here with a specific problem:
- My transition never runs → Section 11 (Edge Cases & Failure Modes)
- I want to understand
pageswap/pagereveal→ Section 4 (Navigation Lifecycle)- I need to debug animations → Section 15 (Diagnostics)
- I want browser internals → Section 9 (Browser Implementation)
- I need compatibility numbers → Section 16 (Browser Compatibility)
- I’m dealing with BFCache weirdness → Section 10 (BFCache Interaction)
1. Introduction
Multi-page applications have always paid a visual tax that single-page applications don’t. A full navigation replaces the entire document - every pixel disappears and reappears, with nothing in between.
For years, the only way to avoid that tax was to stop having pages. Client-side routers simulated navigation entirely in JavaScript, keeping one document alive indefinitely so its DOM could be animated between states.
That trade came with real costs: larger JavaScript bundles, reimplemented history handling, and application code that had to simulate what the browser’s native navigation model already did for free - back/forward semantics, per-page memory isolation, the ability for a page to simply reload from a known-good state, and a browser that could cache, prerender, and evict pages independently because it understood they were genuinely separate documents.
Every one of those properties depends on the browser treating each page as an independent document. A client-side router deliberately gives that up in exchange for animatability. Cross-document view transitions are, structurally, an attempt to get the animatability back without giving up the independence.
Cross-document view transitions remove that trade-off. A real navigation - with its own request, its own document, its own independent JavaScript context - can now produce the same kind of animated, cross-fading transition that single-page routers have offered.
This is defined across two related specifications. CSS View Transitions Module Level 1 defines the same-document model: document.startViewTransition(), the pseudo-element tree, and the core capture-and-animate algorithm. CSS View Transitions Module Level 2 extends that model to work across a same-origin navigation between two entirely separate documents - the subject of this article.
The two documents in a cross-document transition share no DOM, no JavaScript scope, and no direct reference to one another. Producing a single coherent animation out of two independent documents is the central development problem this specification solves, and it shapes nearly every algorithm and failure mode covered below.
This reference follows the discipline established elsewhere on this site: normative specification requirements, Chromium’s specific implementation choices, and production guidance are kept visibly distinct throughout, using consistent framing - “the specification requires,” “Chromium currently implements,” “the specification leaves this implementation-defined,” and, where relevant, “this behavior is observed in Blink rather than guaranteed by the specification.” Where the complete formal algorithm text could not be independently retrieved, that limitation is stated plainly rather than filled in with a plausible-sounding reconstruction - Section 8 covers this in detail, but the practice applies wherever it’s relevant throughout the piece.
Key takeaway: Cross-document view transitions let a genuine multi-page navigation produce the animated transitions previously exclusive to single-page routers, by coordinating two independent documents through a small set of navigation-level events rather than shared state.
2. Specification Architecture
This topic spans more separate specifications than most single-API references need to cover, and readers benefit from seeing that map before the details arrive piecemeal.
| Specification | Governs |
|---|---|
| CSS View Transitions Module Level 1 | Same-document transitions, the pseudo-element model, and the core capture/animate algorithms that Level 2 extends |
| CSS View Transitions Module Level 2 | Cross-document transitions, the @view-transition rule, selective view transitions, nested groups, and scoped view transitions |
| HTML Standard | The pageswap and pagereveal events, the render-blocking mechanism, navigation type classification, and BFCache/prerender activation |
| Navigation API | NavigationActivation and the navigation-type distinctions the navigation descriptor depends on |
| DOM Standard | The base Document and Element interfaces this specification extends |
Level 2 does not redefine capture, animation, or the pseudo-element tree - it depends on Level 1 for all of that, and adds only what’s specific to coordinating a navigation: the opt-in rule, the two new events, and the render-blocking-based stabilization mechanism that replaces the same-document update callback.
The HTML Standard’s role deserves a slightly closer look, because it is not a passive dependency - pageswap and pagereveal are defined in HTML itself, not in the CSS specification, which means the View Transitions module is, structurally, a CSS-layer feature whose triggering mechanism lives entirely outside CSS. This split matters practically: bugs or behavioral questions about when an event fires belong to the HTML Standard and its own navigation algorithm; questions about what happens visually once it fires belong to CSS View Transitions. Conflating the two when filing an issue, or when trying to track down which specification governs a given piece of behavior, is a common and avoidable source of confusion.
The Navigation API’s involvement is narrower than it might appear - it supplies the NavigationActivation object surfaced through pageswap’s activation property and used throughout Section 11’s customization examples, and it supplies the NavigationType classification the navigation descriptor’s auto value depends on. It is a dependency of convenience and precision, not a load-bearing architectural relationship the way HTML’s event definitions are.
Key takeaway: Nothing about how a transition animates is Level 2-specific - that’s inherited entirely from Level 1. Level 2 is exclusively about triggering and coordinating a transition across a navigation boundary, and the fact that the triggering half lives in HTML rather than CSS is itself a useful fact for knowing where to look when something’s timing, rather than something’s visual result, is in question.
3. Mental Model
Four layers, stacked, form the conceptual foundation for everything that follows.
Layer 1 - two documents. The old document and the new document have no shared JavaScript scope, no shared DOM, and exist in entirely separate navigation contexts. Neither can call a method on the other or read the other’s state directly.
Layer 2 - two rendering trees. The “old” and “new” states are not two states of one box tree being diffed - they are two structurally independent box trees, produced by two separate documents, that the browser happens to compare.
Layer 3 - one navigation. The browser’s navigation algorithm is the single thread of control that coordinates the handoff between those two independent documents. pageswap fires from the old document’s side of that handoff; pagereveal fires from the new document’s side. Neither document observes the other - both only observe the navigation passing through.
Layer 4 - one visual illusion. Everything covered in Sections 5 through 7 exists to assemble a single coherent animated illusion out of those four structurally separate pieces. This is precisely why so many of this feature’s real-world failure modes, covered in Section 11, are coordination problems - a name that doesn’t match between two documents, a snapshot taken at the wrong moment, state persisted into a cache the author forgot about - rather than either document misbehaving in isolation.
It is worth being explicit about what this model rules out, because the ruled-out possibilities are exactly what trips developers coming from a same-document background. There is no shared JavaScript object either document can read mid-transition. There is no way for the old document to directly inspect what the new document’s DOM will look like, or vice versa - only URLs, navigation metadata, and whatever the author explicitly serializes through a channel like sessionStorage. And there is no single point in time both documents are simultaneously “alive” in the ordinary sense - the old document’s JavaScript context is gone by the time the new one’s begins running its own handlers. Every mechanism described in the rest of this article - pageswap, pagereveal, the render-blocking stabilization, the captured-elements map - exists specifically to work around that absence of shared state, not despite it.
Key takeaway: Nothing here is one system animating itself. It is two independent documents being made to look like one continuous surface, coordinated only through the navigation itself - which is why coordination, not animation, is where most things go wrong in practice.
4. Navigation Lifecycle
A cross-document transition becomes eligible only when every one of the following holds, per the specification’s own non-normative activation summary:
- Both documents are same-origin.
- The page remains visible throughout the entire navigation.
- The navigation is user-initiated - a click, a form submission, or a
traverse(back/forward) navigation. Navigation via the URL bar is explicitly excluded. - The navigation involved no cross-origin redirects.
- Both the old and new document opted in via the
@view-transitionrule.
| Condition | What disqualifies it |
|---|---|
| Same-origin | Any cross-origin navigation, regardless of opt-in |
| Page visibility | The page becoming hidden at any point during the navigation |
| User-initiated | URL bar entry, bookmark navigation, most reloads |
| No cross-origin redirects | A server-side redirect chain that leaves the origin at any hop |
| Mutual opt-in | Either document lacking the @view-transition rule |
Opt-in itself is deliberately minimal - a CSS at-rule present in both documents, with no JavaScript required for the default cross-fade:
/* In both the outgoing and incoming document's stylesheet: */
@view-transition {
navigation: auto;
}
The navigation descriptor accepts only auto or none (the initial value). auto enables the transition specifically for navigations whose NavigationType is traverse, or push/replace where the navigation’s user involvement is not "browser UI" - which is the formal way the specification excludes URL-bar and similar browser-chrome-initiated navigations from Section 4’s activation table above. The rule can be nested inside @media or @supports, which is how authors commonly disable transitions conditionally - for narrow viewports, or behind a prefers-reduced-motion check, without needing any JavaScript at all.
The lifecycle itself runs in two phases, one per document, connected only by the navigation passing between them.
In the old document: the user initiates navigation; when the new document is ready to be activated, pageswap fires; if the navigation qualifies, the event’s viewTransition property is a live ViewTransition object; the author may customize or skip it; if not skipped, the old document’s state is captured; the navigation then proceeds and the old document is unloaded.
In the new document: at the first rendering opportunity, pagereveal fires, carrying its own viewTransition object whose updateCallbackDone promise is already resolved and whose captured elements are already populated from the old document; the author has a second opportunity to customize or skip; the new document’s state is then captured, and from that point the transition proceeds like a same-document one.
Key takeaway: Activation is an all-or-nothing gate checked before any capture happens - same-origin, visible, user-initiated, redirect-free, and mutually opted-in are not independent preferences but a single joint condition, and any one failing silently disqualifies the entire transition.
5. Rendering Timeline
This is the section most readers arriving with a specific “why isn’t this working” question will want first - and it is the one place in this reference where the normative lifecycle from Section 4 gets fleshed out with implementation-observed timing, explicitly labeled as such throughout.
5.1 Capture timeline (old-document side)
The user clicks a link. Navigation begins. If the old document has render-blocking resources still pending, those are unrelated to the outgoing capture - this document is not waiting on anything new, it is capturing its current, already-rendered state. Rendering pauses within the transition root. The old state is captured - element by element, in paint order, per Section 8. pageswap fires, carrying the ViewTransition object. The old document is unloaded and the navigation proceeds.
5.2 Transit (network)
The response arrives; the new document begins parsing. This step is implementation-timed, not normatively sequenced relative to the old document’s teardown - the specification does not mandate an exact interleaving here, only that the new document’s capture cannot happen before it exists.
5.3 Stabilization (new-document side)
This is where cross-document transitions diverge most visibly from same-document ones. A same-document transition has an explicit updateCallback - the author’s code runs, and its returned promise settling is the deterministic signal that the new state is ready to capture.
A cross-document transition has no such callback, because there is no shared script to call. Instead, the browser relies entirely on the render-blocking mechanism: scripts, styles, and elements marked blocking="render" (via the script/style/link blocking attribute, or an expect <link> targeting a specific element) delay the first render - and therefore delay the point at which pagereveal fires and new-state capture happens.
5.4 Animation
pagereveal fires. New-state capture runs. The pseudo-element tree is generated (Section 6). Rendering unpauses, revealing the tree. ready fulfills - the correct moment to attach custom Web Animations. The pseudo-elements animate. finished fulfills once the end state is fully visible and interactive. The transition pseudo-elements are removed, and the page is now, indistinguishably, an ordinary page.
The second diagram makes explicit what the sequence diagram alone doesn’t: which boundaries are fixed by the specification and which are left to engine timing.
Key takeaway: The old-document side and new-document side each have their own normatively fixed internal ordering, but the gap between them - how long the network transit and render-blocking stabilization take - is not something the specification bounds. A slow response or an overused render-blocking mechanism stretches that gap, and the user simply sees the old page held frozen for longer.
6. Rendering Pipeline & Pseudo-Element Tree
Once both states are captured, the browser builds a tree of pseudo-elements representing the transition - rooted at ::view-transition, originating from the transition’s root element (the document element, for a cross-document transition).
::view-transition
├─ ::view-transition-group(name)
│ └─ ::view-transition-image-pair(name)
│ ├─ ::view-transition-old(name)
│ └─ ::view-transition-new(name)
└─ …one group per captured view-transition-name…
::view-transition-group() is generated once per unique view-transition-name and mirrors that element’s size and position; if both an old and new state exist, its width, height, and transform are animated between them by a dynamically generated stylesheet. ::view-transition-image-pair() exists purely to provide isolation: isolate for its children, so non-default blend modes can be applied without leaking into the rest of the page. ::view-transition-old() and ::view-transition-new() are the actual visual snapshots - each a replaced element, omitted entirely if that side has no corresponding state.
The document element is given view-transition-name: root by the user-agent stylesheet, which is why every transition has at least one group even with no author-assigned names.
The snapshot containing block
All of this is laid out relative to the snapshot containing block - a rectangle covering everything that could potentially display page content, deliberately independent of scrollbars or on-screen interactive widgets like a virtual keyboard. It functions similarly to the containing block position: fixed establishes, and it is both the absolute- and fixed-positioning containing block for ::view-transition and its descendants.
Painting order
The specification introduces a new stacking layer - the view transition layer - appended to the end of the standard CSS2 painting order, after all other content including the top layer. This placement is deliberate: capturing the top layer’s contents into a snapshot while also being part of the stacking contexts being captured would create a circular dependency, so the view transition layer is instead a sibling of everything else, not a descendant of it.
While a transition is in its animating phase, any element captured into the transition stops painting at its normal DOM location - as if opacity: 0 - and stops responding to hit-testing - as if pointer-events: none - because its visual representation now lives entirely in the corresponding ::view-transition-new() pseudo-element instead. Assistive technology access is explicitly unaffected by this; the accessibility tree does not expose the pseudo-element tree at all, and continues reflecting the real DOM.
This a11y-tree exemption is worth dwelling on, because it resolves a question that would otherwise be a genuine accessibility concern: if the visual and accessibility representations of a captured element diverged during the animating phase - the pixel content living in a pseudo-element the a11y tree can’t see, while assistive technology continues reading from a DOM node that isn’t currently painting anything - a screen reader user could be left interacting with content that doesn’t visually correspond to what’s on screen. The specification avoids that divergence by design: assistive technology never sees the pseudo-element tree at all, so from its perspective nothing has changed about the DOM’s accessible structure, only its paint and hit-test behavior for the (necessarily brief) duration of the animation.
There is a related, more subtle consequence of “captured elements form a stacking context” from Section 3.1.1’s rendering-consolidation rule: any element with a non-none view-transition-name, whether or not it’s currently part of an active transition, permanently forms a stacking context, is flattened in 3D transforms, and forms a backdrop root. This is not conditional on a transition being active - assigning a view-transition-name to an element changes how that element participates in stacking and 3D transform grouping at all times, which is a small but real authoring surprise: adding view-transition-name: hero to prepare an element for a future transition can silently change how it composites with siblings even before any transition ever runs.
User-agent stylesheet
The default styling that makes any of this work without author CSS establishes: absolute positioning for the whole tree relative to the snapshot containing block; a default 0.25-second cross-fade animation-duration inherited down through the tree; inset-block-start: 0, inline-size: 100%, block-size: auto on the old/new image pseudo-elements - specifically without an object-fit override, meaning these replaced elements use the initial fill behavior by default. Authors transitioning elements whose aspect ratio changes between old and new states need to set object-fit: cover explicitly, or the default stretch-to-fill behavior will visibly distort the image.
In outline, the user-agent stylesheet does roughly this - reconstructed here for the shape of the defaults rather than as a verbatim quotation:
:root { view-transition-name: root; }
::view-transition { position: absolute; inset: 0; }
::view-transition-group(*) {
position: absolute; top: 0; left: 0;
animation-duration: 0.25s;
animation-fill-mode: both;
}
::view-transition-image-pair(*) { position: absolute; inset: 0; }
::view-transition-old(), ::view-transition-new() {
position: absolute; display: block;
inset-block-start: 0; inline-size: 100%; block-size: auto;
/* no object-fit set - defaults to "fill" */
}
Animation-related properties are also inherited down through image-pair, old, and new from their parent group, which is why setting animation-duration once on a ::view-transition-group() propagates to its descendants without needing to repeat it - the same pattern used in the header/sidebar customization examples common in Level 1 documentation.
Scoped view transitions
Everything described so far assumes a global view transition - one rooted at the document element, rendered in its own top-level view transition layer above the entire page. The specification also permits scoped view transitions, hosted on an individual element rather than the document. A scoped transition requires that element to establish layout containment, so its painted output can be captured as a single atomic unit; its ::view-transition pseudo-element is then painted as a stacking context above that element’s own other children, rather than in the document-wide transition layer. This is a same-document-only capability at present - cross-document transitions, by definition, are always global, since the entire document is what’s being replaced.
Key takeaway: The pseudo-element tree, the snapshot containing block, and the view transition layer together form a rendering surface deliberately isolated from the rest of the page’s stacking and layout - which is exactly what lets a transition animate cleanly over content it doesn’t otherwise interact with, at the cost of authors needing to know this isolated layer exists when something looks wrong.
7. Shared Element Matching
An element participates independently in a transition by being tagged with a view-transition-name. Two elements - one in the old state, one in the new - sharing the same name are treated as two visual states of the same conceptual entity, and are paired into a single ::view-transition-group(), regardless of whether they are, DOM-wise, the same element at all.
This is the mechanism behind “shared element” transitions - a thumbnail in a list and a hero image on a detail page can share a name and animate as if one grew into the other, even though they are two entirely different elements in two entirely different documents.
The specification is explicit about the failure mode this creates: if two elements simultaneously specify the same view-transition-name, the transition aborts. Non-unique names are permitted only as long as the elements bearing them are never part of the same transition - which, as Section 10 covers, is precisely the trap BFCache-persisted names fall into.
view-transition-scope (none | all) limits where a view-transition-name can be discovered from - setting it to all means transitions originating outside that element’s subtree won’t see names inside it, useful for keeping independently-scoped transitions from interfering with each other.
view-transition-group builds hierarchy on top of the otherwise-flat pseudo-element tree. Its values - normal, contain, nearest, or a specific <custom-ident> - control which ancestor ::view-transition-group() a given element’s own group nests under, generating an intermediate ::view-transition-group-children() pseudo-element to hold it.
.container { view-transition-name: container; }
.container, ::view-transition-group-children(container) {
clip-path: circle();
}
article {
view-transition-name: article;
view-transition-group: container; /* nest under .container's group */
}
Without nesting, every pseudo-element sits flat under ::view-transition, clipped only to the snapshot containing block - which means clip-path, opacity, filters, and 3D transforms applied to a containing element in the real DOM don’t carry over to its descendants’ pseudo-elements. Nesting exists specifically to preserve that parent-child visual relationship through the transition.
Selective view transitions
A page with more than one kind of transition - sliding the whole page on primary navigation, versus reordering a single list on drag-and-drop - needs a way to scope styling to which transition is currently running, not just to which elements are captured. Level 2 introduces active types for exactly this: a ViewTransition carries a set of arbitrary author-defined type strings, populated either via startViewTransition({types: [...]}), mutated later through the transition’s own types property, or - for cross-document transitions specifically - declared through the types descriptor on the @view-transition rule itself.
Two new pseudo-classes select on this state. :active-view-transition matches the document element whenever any transition is currently active, regardless of type. :active-view-transition-type() matches only when the active transition’s types intersect the ones listed:
:root:active-view-transition-type(slide-in) { /* ... */ }
:root:active-view-transition-type(slide-in, reverse) { /* matches either */ }
:root:active-view-transition { /* matches any active transition */ }
For cross-document transitions, types are typically assigned inside the pagereveal handler by inspecting the navigation’s origin and destination - the Stack Navigator pattern common in cross-document demos, where the direction of navigation (forward into a detail view versus backward to an overview) determines which slide direction the shared styling should use, entirely through type-based selectors rather than per-page bespoke CSS.
Sharing styles with view-transition-class
A separate Level 2 addition addresses a related but distinct scaling problem: with dozens or hundreds of independently named elements, writing per-name CSS for each one becomes unmanageable, since view-transition-name values must be unique. view-transition-class decouples the identity of a captured element (its unique name) from the styling hook used to target it - multiple elements can share a class even though none of them can share a name, letting one block of CSS style an entire category of transitioning elements at once rather than one rule per element.
Key takeaway: Shared element matching is name-based, not DOM-based - which is what makes cross-document “this thumbnail becomes that hero image” transitions possible at all, but it also means a name collision is a silent, total transition-abort, not a partial failure.
8. Normative Processing Model
The complete formal algorithm text for this specification’s cross-document setup algorithms - Setup cross-document view transition and its constituent steps - could not be retrieved in full through available tooling; the source document exceeds what the available fetch mechanisms return, and the raw file is not independently accessible. What follows is built only from algorithm fragments, definitions, and behavior independently confirmed through the published specification text, its data-structure definitions, and its own tracked issue discussions - not a reconstruction of the missing formal steps. Readers needing the complete formal text should consult the live specification directly.
Capture ordering. Capture the old state iterates elements in paint order, populating the transition’s named-elements map in that order. A tracked specification discussion (CSSWG issue #9672) considered whether capture the new state’s handling of elements with no old-state counterpart preserves that ordering when appending late entries - relevant specifically to entry animations, where paint order at the moment of insertion may not match final visual stacking.
Capture exclusions. Elements that are fragmented, whose contents are skipped (per CSS containment), or that are not rendered are excluded from capture. This was, for a time, inconsistently applied between old-state and new-state capture - a discrepancy tracked and closed as a specification bug (issue #11071), now aligned so both capture steps apply the same exclusions. Specifically, WebKit does not implement the fragmentation check at all, because - per WebKit developers’ own comments in the CSSWG’s public archive - there is no reliable way to detect fragmented content in that engine. This is a genuine, acknowledged cross-engine behavioral difference, not merely an oversight.
Rendering suppression. A document-level flag - rendering suppression for view transitions - is set true for the duration of capture and false afterward. This is the actual mechanism underlying “rendering is paused” in the lifecycle description; it is not a separate, informally-described pause but a defined boolean the rendering pipeline checks.
Outbound post-capture steps. The specification defines a hook - outbound post-capture steps - connecting the old document’s capture completion to the point where navigation is allowed to proceed. startViewTransition()’s own method steps interact with this directly: if a same-document transition is started on a document whose outbound post-capture steps are already set (i.e., a cross-document transition’s capture is in flight), that new same-document transition is immediately skipped - specifically to guarantee that a same-document transition started after pageswap has already fired cannot interfere with the outbound cross-document one.
Deferred state restoration. A merged specification fix defers restoring persisted state - such as scroll position - until after old-state capture completes, rather than before it. This was motivated by a concrete authoring pattern: calling document.startViewTransition() from inside a popstate handler, where restoring scroll position too eagerly would corrupt the very snapshot the transition is trying to capture.
Cross-origin redirect handling. Transitions are currently disabled entirely for navigations into a document created via a cross-origin redirect. This check explicitly does not apply when a document is being reactivated - from BFCache, for instance - since in that case any cross-origin redirect already happened during the original navigation, not the reactivation. The restriction is scoped to server-side redirects only; a client-side redirect (a meta http-equiv="refresh", for example) is treated as an entirely new, independently-evaluated navigation.
Design history: how the algorithm arrived at its current shape. The original proposal that became this section (issue #8804) sketched a noticeably different mechanism than what shipped: rather than the pageswap event, it proposed a dedicated ViewTransitionBeforeCaptureEvent, fired on the old document specifically to let authors decide which elements to tag based on the destination URL - before any state was captured. That event was superseded in the shipped design by the more general-purpose pageswap, which serves the same “customize before capture” purpose but is defined independent of whether a transition ends up happening at all, giving authors one consistent event to reason about instead of two. Tracing this lineage matters less for using the API than for reading older blog posts and Stack Overflow answers accurately - content describing ViewTransitionBeforeCaptureEvent predates the current design and does not reflect shipped behavior.
Render-blocking stabilization, in full. Section 5.3 introduced the render-blocking mechanism as the cross-document substitute for the same-document update callback. The specification’s own example makes the mechanism concrete:
<head>
<!-- Render-blocking by default: -->
<link rel="stylesheet" href="style.css">
<!-- Explicitly marked render-blocking, since this script
fixes up layout the transition depends on: -->
<script async src="fixup.js" blocking="render"></script>
<!-- Blocks the first render until this specific element
has been seen and fully parsed: -->
<link rel="expect" href="#main-article" blocking="render">
</head>
The last frame of the old document remains visible, and the animation itself is delayed, until all three conditions resolve - style.css applied, fixup.js executed, and the #main-article element fully parsed. This is the direct mechanism behind Section 5.3’s “implementation-timed, not normatively sequenced” characterization of network transit: the specification defines what must resolve before capture, not how long that resolution takes.
| Category | Example | What it means for implementers |
|---|---|---|
| Normative, confirmed | Capture exclusions; rendering suppression flag; deferred state restoration; cross-origin redirect gate | Verified against specification text and its own change history |
| Normative, engine-divergent | Fragmentation exclusion check | Specification requires it; WebKit does not implement it |
| Confirmed but not verbatim | Full step-by-step text of Setup cross-document view transition | Behavior described here is accurate; exact formal wording was not retrievable |
| Design-history only | The original ViewTransitionBeforeCaptureEvent proposal | Superseded by pageswap in the shipped design; included here only as design-history context |
Key takeaway: Everything in this section is either directly sourced from specification text or explicitly labeled as unverified - the formal algorithm steps that could not be retrieved are not filled in with plausible-sounding reconstruction, because a wrong “confirmed” fact is worse than an honest gap.
9. Browser Implementation
Everything above is specification behavior. This section describes one engine’s implementation - Chromium’s - labeled explicitly as implementation detail, not a normative requirement any other browser must replicate.
9.1 Blink Architecture
The feature is tracked under its own Blink component, Blink>ViewTransitions, separated out from the general Blink>CSS bucket used during the earliest prototyping stage. The single-page-app (same-document) API shipped through its own Intent to Ship, with cross-document support explicitly deferred to - and delivered through - a separate follow-up Intent to Ship, a genuine two-phase rollout rather than one feature landing all at once.
Blink’s own internal documentation - found under the document_transition implementation directory, which research for this article could not confirm is still the current directory name versus a legacy holdover from before the API’s standardization - describes the core mechanism in terms that map directly onto the specification’s capture model: during a transition, Blink runs the document lifecycle phases up through paint to produce what that documentation calls a “sparse post-layout representation” - tracked only for the subset of elements the specification calls captured elements, not the whole tree. For each such element, the browser records the state actually needed to render it as a standalone image: its bounding box size, the transform mapping that box into viewport space, and its relative paint order against other captured elements. This is the implementation-level shape of what Section 8 describes normatively as “capture the old/new state.”
9.2 Renderer Process
Capture, DOM bookkeeping for the pseudo-element tree, and style resolution for the dynamically generated transition stylesheet all happen in the renderer process - the same process running the page’s own script and layout. This is also where the rendering suppression flag from Section 8 is checked and enforced during the browser’s rendering lifecycle phases.
9.3 Viz Compositor
Where this feature genuinely leaves the renderer process: Chromium’s components/viz/ - the GPU-process display compositor - contains a dedicated Transitions subdirectory built specifically for the view transitions project, explicitly cross-referencing the renderer-side implementation directory in its own documentation.
Captured snapshots are not simply bitmaps held in renderer memory - they flow through Chromium’s standard cross-process compositing pipeline: individual CompositorFrames reach the GPU process’s SurfaceManager, get combined by the SurfaceAggregator, and are eventually drawn by viz::DirectRenderer. This is the same pipeline every other composited layer in the browser uses; view transitions plug into existing infrastructure rather than inventing a parallel one.
This architectural choice has a direct, testable consequence for where a transition can go wrong: because captured content is composited through the same Surface-based indirection Chromium already uses for cross-process content like out-of-process iframes, a rendering bug specific to view transitions is more likely to manifest as a compositing or surface-aggregation issue than as a renderer-process bug in the traditional sense. This is a reasonable inference from the confirmed architecture, not an independently verified claim about where bugs actually cluster in practice - no bug-tracker analysis was performed to confirm it empirically.
9.4 Snapshot Capture
At the texture level, a captured element’s image is backed by the same GPU resource abstractions Chromium uses throughout its compositor - cc’s TransferableResourceLayer/SurfaceLayer machinery, with cross-process texture sharing handled via SharedImage, and memory usage tracked in Chromium’s own GPU memory-tracing tooling under its CC and GPUMemoryBuffer categories. This grounds the general architecture of how a snapshot becomes GPU-resident memory. No view-transition-specific published figures for typical texture memory overhead - per-page, per-named-element, or otherwise - were found during research for this section; that absence is disclosed rather than estimated, and is picked up again in Section 13.
One architectural detail is confirmed and worth stating precisely, since it clarifies why capture has a real, non-trivial cost rather than being a cheap metadata operation: producing a captured element’s image requires actually rendering that element’s paint output into a texture, not merely recording its geometry. The bounding-box, transform, and paint-order bookkeeping described in Section 9.1 is metadata; the image itself is a full rasterized (or GPU-rasterized) representation of that element’s visual appearance at capture time, which is why fragmented or off-screen content - excluded per Section 8’s capture rules - would otherwise represent wasted rasterization work the specification’s exclusions are partly designed to avoid.
9.5 Two-Phase Capture Prototype
Today’s implementation captures a cross-document transition’s outbound state only once the new document’s network response has arrived, and activates it only once the new document is ready to produce its first frame. When that gap is long, the old page sits visibly frozen.
Chromium has an active prototyping effort - tracked as a formal Intent to Prototype - for a two-phase capture model: starting a same-document-style transition at the moment navigation begins, with a guarantee that it continues through to completion as long as the new document is ready to present frames before the navigation actually commits. As of the tracked intent, it remains genuinely unclear whether this requires a specification change or can be achieved purely as an internal Chromium scheduling improvement - Gecko and WebKit had given “No signal” on it, while web developer sentiment on the linked explainer was reported positive.
Key takeaway: This feature’s implementation reaches meaningfully past the renderer process into the GPU-process compositor - captured snapshots are ordinary composited GPU resources flowing through Chromium’s standard display pipeline, and the most consequential unresolved implementation question (two-phase capture) is about when capture happens relative to navigation, not how the pixels themselves get rendered.
10. BFCache Interaction
pagereveal fires uniformly across three distinct activation paths: a fresh network load, a back/forward cache restoration, and a prerender activation. This is not three separate mechanisms coincidentally sharing a name - Web Platform Tests specifically verify event-ordering consistency across exactly these three cases (order-in-bfcache-restore.html, order-in-new-document-navigation.html, and order-in-prerender-activation.html, all under the HTML specification’s history-traversal test suite), confirming this is treated as one unified firing point at the specification level.
That unification is also the source of the single most-documented cross-document failure mode. If author code assigns view-transition-name values inside a pageswap or pagereveal handler and does not remove them once the snapshot has been taken, those names persist into the page state BFCache saves. On a subsequent back-navigation, the restored page’s own pagereveal handler may run again and attempt to reassign the same names to different elements - triggering the duplicate-name-abort rule from Section 7, silently.
The specification’s own example code establishes the idiomatic cleanup pattern directly: assign names inside pageswap, and use the returned ViewTransition’s finished promise - which resolves regardless of whether the transition succeeded, skipped, or was later restored from BFCache - as the point to clean that state back up.
window.addEventListener("pageswap", event => {
if (!event.viewTransition) return;
thumbnail.classList.add("transition-to-hero");
// Cleans up even if this page is later restored from BFCache.
event.viewTransition.finished.then(() => {
thumbnail.classList.remove("transition-to-hero");
});
});
pageswap itself has BFCache-relevant behavior baked into its own definition: when a transition is present, it fires before the document is hidden - deferring activation of the incoming document in favor of one final rendering update to capture the outgoing state. For navigations that don’t qualify for a transition, pageswap still fires, immediately before pagehide, but only for the navigating document itself - iframes, and any document unloaded without a navigation at all, never receive it.
There is a second, less obvious BFCache interaction worth separating from the name-persistence problem above: BFCache preserves a document’s entire JavaScript heap, including any timers or in-flight promises, and resumes them essentially unmodified on restoration. A ViewTransition object’s promises - ready, finished, updateCallbackDone - are ordinary promises subject to the same freeze-and-resume behavior as everything else in a BFCached page. If application code holds a reference to a transition object and expects its promises to have settled by the time the page is restored, that assumption needs revisiting: the transition that was active when the page was frozen is not necessarily the transition active after restoration, since a fresh pagereveal - with a fresh ViewTransition object - fires on every reactivation, per the unified firing behavior confirmed by the WPT tests above.
This is also why the specification’s own cleanup pattern anchors on finished rather than on any assumption about page lifetime: finished is scoped to one specific transition’s lifecycle, not to the page’s, and resolves correctly whether the page goes on to be BFCached, closed, or simply navigated away from normally.
Key takeaway: BFCache doesn’t break cross-document view transitions - it exposes a bug class in transition-name lifecycle management that would otherwise stay latent, because BFCache is precisely the mechanism that makes “state assigned in a previous navigation’s handler is still around” observable at all.
11. Edge Cases & Failure Modes
Nearly every entry below can be traced back to one of two root causes established in Section 3: either the joint activation gate in Section 4 failed silently, or one document made an assumption about the other’s state that the coordination channels available - pageswap, pagereveal, sessionStorage, or persisted cache state - didn’t actually guarantee. Reading each failure mode with that framing in mind makes the underlying pattern easier to recognize the next time a new, undocumented variant of it shows up.
Duplicate view-transition-name. Covered in Section 7: two elements sharing a name within the same transition aborts it outright, with the BFCache-triggered variant in Section 10 being the most commonly reported real-world instance.
Cross-origin redirects. Per Section 8, any server-side redirect chain leaving the origin currently disables the transition entirely for that navigation, with no partial-eligibility fallback. This is a live open question (issue #11063) rather than settled permanent behavior - see Section 17.
Browser-UI-initiated navigation. The navigation: auto descriptor’s own definition excludes URL bar entry, bookmark navigation, and most reload types by design, not by omission.
Excluded elements. Fragmented elements, elements whose contents are skipped, and not-rendered elements are excluded from capture on both the old and new side (Section 8), with WebKit’s fragmentation-check omission as the one confirmed cross-engine divergence in this area.
Snapshot containing block size changes. If the snapshot containing block’s size changes mid-transition - a viewport resize, for instance - the transition skips, tracked via the ViewTransition’s own initial snapshot containing block size internal state and a linked specification discussion (issue #8045).
Page hidden during navigation. The specification’s own change history for this Level records a fix ensuring that when a document is hidden, an inbound cross-document transition is skipped rather than attempted against a page the user cannot currently see.
iframe support. Chrome’s own 127 release notes document a concrete, dated fix here: previously, document.startViewTransition() inside a same-origin iframe would not work if the main frame was simultaneously running its own transition; both same-document and cross-document transitions inside same-origin iframes are supported as of that release. Earlier, less specific reports of iframe-related instability circulated informally; this release-notes fix is the only claim in this category independently verified against a primary source, and it’s the only one presented here.
Customizing eligibility inside the handlers themselves. Beyond the joint activation gate, authors have a second layer of control - deciding, inside pageswap and pagereveal, whether a transition that is eligible should actually run. The specification’s own example illustrates the shape of this, combining a navigation-type check with element-specific state:
// Old document:
window.addEventListener("pageswap", event => {
if (!event.viewTransition) return; // Not eligible - nothing to customize.
// Skip transitions specifically for back/forward navigation:
if (event.activation.navigationType === "traverse") {
event.viewTransition.skipTransition();
}
const destination = new URL(event.activation.entry.url);
if (destination.pathname === "/details" && thumbnail.complete) {
thumbnail.classList.add("transition-to-hero");
// Cleans up even if this page is later restored from BFCache:
event.viewTransition.finished.then(() => {
thumbnail.classList.remove("transition-to-hero");
});
}
});
event.activation - a NavigationActivation from the Navigation API - is what makes this decision possible without any communication channel to the other document: it exposes the navigation type and the destination URL after redirects, entirely from information the navigation itself already has.
| Symptom | Likely cause | Citation |
|---|---|---|
| Transition never runs at all | One of the joint activation conditions failed (Section 4) | Spec §8.1.1 |
| Transition aborts with no visible error | Duplicate view-transition-name, often BFCache-persisted | Spec §3.1; Section 10 |
| Works forward, not on back-navigation | Names assigned in a handler weren’t cleaned up before BFCache save | Section 10 |
| Old page freezes for a long time | Render-blocking mechanism overused on the new document | Spec §8.1.2 |
| Image looks stretched during the transition | Default object-fit: fill on ::view-transition-old/new() | Section 6 |
Key takeaway: Nearly every failure mode here traces back to either the joint activation gate (Section 4) or the coordination problem the mental model in Section 3 predicted - a mismatch between what one document assumes and what the other actually did.
12. Security and Privacy
The specification’s formal Security Considerations and Privacy Considerations sections exist but their complete text could not be independently retrieved this round - the disclosure pattern from Section 8 applies here as well. What follows is grounded in the specification’s own informative appendix commentary and its linked design discussions, not the full formal sections.
The structural mitigation doing most of the work is the activation gate itself: same-origin only, no cross-origin redirects. A transition simply cannot observe or animate content from a different origin, by construction, not by a separate check layered on top.
The specification’s own appendix text flags one specific, narrower concern directly: this feature makes CSS aware of navigation for the first time - previously, CSS had no way to know anything about where a page was navigating to or from. The appendix acknowledges this “can raise concerns around safety [of] 3rd-party CSS,” while noting the general mitigating principle that third-party stylesheets should already come from trusted sources, since CSS can already learn about and modify a document in many other ways. This specific concern is tracked in two linked discussions (CSSWG issue #8684 and a corresponding WICG view-transitions issue).
The redirect-related information exposure question - whether a same-origin redirect chain should be treated more permissively than the current blanket cross-origin-redirect restriction - is the same open issue (#11063) referenced in Section 11’s failure-mode entry and Section 17’s open questions.
It is also worth being precise about what the same-origin restriction does not by itself guarantee. Two same-origin pages can still differ substantially in what they expose to each other through the coordination channels this specification does define - sessionStorage, the NavigationActivation object’s URL and navigation-type fields, and whatever an author chooses to pass through those channels deliberately. None of that is a specification-level information leak; it is the intended, documented surface authors use to coordinate a transition, covered throughout Sections 4, 10, and 14. The distinction matters because it separates what the specification restricts structurally (cross-origin access, full stop) from what remains an ordinary same-origin authoring responsibility (not passing sensitive data through a coordination channel that doesn’t need it).
Key takeaway: The dominant mitigation is structural (same-origin, redirect-free) rather than a discretionary check a developer can misconfigure; the one specifically flagged residual concern is that this feature makes navigation state newly visible to CSS, a genuinely new category of information exposure for a language that previously had none.
13. Performance Characteristics
The mechanical cost model here is: capturing an element into a transition means generating a rendered image of it, and every named element adds one more such image, one more ::view-transition-group(), and one more compositor-managed texture to the transition.
Section 9.4 already disclosed that no published, view-transition-specific figures for typical texture memory overhead were found. That gap is real and stated here again deliberately, rather than papered over with an estimate: this section discusses characteristics, not measurements.
Capture cost scales with how many elements carry a view-transition-name and how large their rendered output is - a full-viewport root transition is one capture; a product grid with fifty independently named cards is fifty, each needing its own rendered image and its own texture upload into the pipeline described in Section 9.3.
Compositor work during the animated phase is comparatively cheap by design: the default and dynamically generated animations operate on width, height, transform, and opacity - properties the compositor can animate without triggering layout or paint on every frame, the same category of “compositor-friendly” properties any other CSS animation would want to stick to for smooth performance.
Large pages and many named elements are the practical risk area implied directly by the capture-cost model above, even without a specific memory number to cite: more named elements means more simultaneous captured images held in GPU memory for the duration of the transition, not just at its start.
The cross-document case adds a dimension the same-document model doesn’t have. A same-document transition captures and animates within a page whose total memory footprint the author already controls and has presumably already budgeted for. A cross-document transition briefly holds captured state from two documents’ worth of content simultaneously - the old page’s snapshots persist until the transition’s finished promise resolves, overlapping with whatever memory the new page’s own initial render requires. This overlap is inherent to the feature’s design, not a bug, but it is a genuine reason the “how many named elements is too many” question in Section 14’s just-in-time-naming guidance matters more here than it would for a purely same-document transition.
Animation duration itself is a performance lever, not just an aesthetic one. The 0.25-second default from Section 6’s user-agent stylesheet is a deliberate compromise; shorter transitions reduce the window during which two documents’ worth of captured state coexists in memory, at the cost of a less pronounced visual effect. This is not a formally specified trade-off, only a direct consequence of the mechanism described above.
Key takeaway: The performance story here is architecturally sound - captures flow through the same GPU-resident, compositor-driven pipeline as any other composited content - but the scale at which that stops being cheap (how many named elements, how large, on what hardware) is not something this research could pin down with authoritative numbers, and neither should the article pretend otherwise.
14. Production Guidance
Progressive enhancement is the default posture, not an opt-in choice. The specification’s own design principle - a transition is a visual enhancement around a state change, never a gate on it - means a misconfigured or unsupported transition never blocks the underlying navigation. Feature-detect with @supports not (view-transition-name: none) and treat unsupported browsers as simply getting an ordinary navigation, because that is precisely the specified fallback behavior, not a workaround authors have to build themselves:
@supports not (view-transition-name: none) {
/* Browsers here don't support the property at all -
no transition-specific CSS should assume it exists. */
}
Testing for view-transition-name support this way is a reasonable proxy specifically because it’s a Level 1 property; a browser recognizing it is very likely to also recognize the @view-transition rule and cross-document activation, since both modules ship from the same implementation effort in every engine that has shipped either so far. It is a proxy, not a guarantee - Section 16’s compatibility table remains the authoritative source for which specific capability a given engine version actually supports.
Respect prefers-reduced-motion without disabling the feature. Setting animation-duration: 0s !important on ::view-transition-group(), ::view-transition-old(), and ::view-transition-new(*) inside a @media (prefers-reduced-motion: reduce) block suppresses the animation while leaving the underlying DOM update and navigation completely intact - the transition still runs, it simply completes instantly.
Use the render-blocking mechanism sparingly. The specification warns about this directly: over-relying on blocking="render" to stabilize the new document can leave the old page’s frozen last frame on screen far longer than intended, turning a smooth transition into a stall. Mark only what genuinely must be present before the transition proceeds.
Set object-fit: cover deliberately, not defensively everywhere. Per Section 6, the default UA styling leaves ::view-transition-old()/::view-transition-new() at the initial fill behavior. Wherever a captured element’s aspect ratio can differ between old and new states, an explicit object-fit: cover (or another deliberate choice) avoids visible stretching - this follows directly from the confirmed UA stylesheet text, not from guesswork.
Assign names just-in-time for scale. Rather than hard-coding a unique view-transition-name on every one of dozens or hundreds of elements up front, assign names inside the pageswap/pagereveal handlers based on what’s actually about to transition, and remove them once finished resolves - the same pattern Section 10’s BFCache cleanup example already demonstrates for a different reason. This pattern is reported to be what some third-party routing tools use internally, though the specific internal implementation of any given tool was not independently verified for this article.
Coordinate custom animations through pagereveal, not pageswap. Because the new document’s ViewTransition is populated with the old document’s captured state by the time pagereveal fires, that - not pageswap - is where custom Web Animations belong. The specification’s own worked example demonstrates a cursor-anchored circular reveal built entirely this way, passing the click coordinates from the old document to the new one via sessionStorage since no direct channel exists between them:
// Old document - record where the user clicked:
addEventListener("click", event => {
sessionStorage.setItem("lastClickX", event.clientX);
sessionStorage.setItem("lastClickY", event.clientY);
});
// New document - runs on both a fresh load and a BFCache reactivation:
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;
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)" }
);
});
Two details worth carrying forward from this example specifically: await event.viewTransition.ready before touching any pseudo-element with the Web Animations API - animating too early, before the pseudo-element tree exists, has nothing to attach to - and the explicit acknowledgment in the handler’s own comment that pagereveal fires identically on a fresh load and a BFCache restoration, which is exactly Section 10’s unification point showing up in practice, not just in the specification text.
Key takeaway: Every piece of guidance here traces back to a specific, cited spec behavior - the enhancement principle, the render-blocking warning, the UA stylesheet’s default object-fit, and the BFCache name-cleanup pattern - rather than generic best-practice advice detached from the mechanism.
15. Diagnostics
Chrome’s own Intent to Ship documentation confirms the feature is debuggable through two existing DevTools surfaces rather than a dedicated panel: the Animations panel can pause and scrub through every animation running on the generated pseudo-elements, and the generated pseudo-DOM structure is inspectable and targetable in the Styles panel, exactly like ordinary DOM elements.
No dedicated tracing category or Performance-panel-specific view-transition marker was confirmed during research for this article - that gap is disclosed rather than assumed, consistent with the rest of this reference’s sourcing discipline.
Writing a minimal reproduction
For any suspected specification-level bug, isolate it to the smallest possible pair of documents before assuming a browser defect. A minimal cross-document reproduction needs: two static HTML files, each with an @view-transition { navigation: auto; } rule, linked to each other, with no framework, bundler, or client-side routing involved. If the transition works between those two bare files but not in the application, the fault almost certainly lies in application-level coordination - a missing opt-in on one side, a name collision, or handler logic interfering with activation - not in the browser’s implementation of the specification itself. This mirrors the same isolate-first discipline recommended for scheduler.yield() diagnostics in this publication’s companion reference, and for the same reason: it cheaply separates “the platform is doing something unexpected” from “my code has a bug,” which are easy to conflate when a failure is silent, as most of this feature’s failures are (Section 11).
Confirming activation without guessing
Because a disqualified navigation fails silently - no console error, no rejected promise, simply an ordinary navigation where a transition was expected - the most reliable diagnostic is to check event.viewTransition directly inside a pageswap or pagereveal handler rather than assume eligibility from the CSS alone:
window.addEventListener("pageswap", event => {
console.log("View transition eligible:", event.viewTransition !== null);
if (event.viewTransition) {
console.log("Navigation type:", event.activation?.navigationType);
}
});
A null viewTransition here, on a navigation the author expected to qualify, points directly back to Section 4’s activation table rather than to anything animation-related - confirming eligibility should always be the first diagnostic step, before investigating pseudo-element styling or timing.
Web Platform Tests offer the most authoritative ground truth available for pagereveal timing specifically:
| Test file | Behavior verified |
|---|---|
order-in-new-document-navigation.html | pagereveal ordering on an ordinary fresh navigation |
order-in-bfcache-restore.html | pagereveal ordering when the document is restored from BFCache |
order-in-prerender-activation.html | pagereveal ordering when a prerendered document is activated |
(All under html/browsers/browsing-the-web/history-traversal/pagereveal/tentative/ in the Web Platform Tests repository.)
| Symptom | Likely cause | Fix |
|---|---|---|
| Transition never runs | Navigation excluded from activation (URL bar, reload, cross-origin redirect) | Confirm the navigation type against Section 4’s activation table |
| Works one direction, not the other | Only one document has the @view-transition opt-in | Both old and new documents must opt in |
| Aborts with no visible error | Duplicate view-transition-name, often BFCache-persisted | Check for names not cleaned up after finished (Section 10) |
| Old content flashes before the transition starts | Render-blocking mechanism overused on the new document | Reduce what’s marked blocking="render" |
| Works in Chrome, does nothing elsewhere | Cross-document support is Chromium/Safari-only as of this writing | Feature-detect; treat as progressive enhancement (Section 14) |
Key takeaway: DevTools support here is real but general-purpose - the Animations and Styles panels, not a feature-specific tool - so a minimal two-page reproduction plus the WPT ordering tests above are the more reliable way to isolate genuinely spec-level behavior from an application-level mistake.
16. Browser Compatibility
Support tables answer “does it exist.” Understanding the mechanism, covered in every section before this one, matters more for a reference like this than the version numbers do - which is why compatibility comes near the end rather than up front.
Cross-document view transitions: Chrome 126 (June 2024), Edge 126 (June 2024), Safari 18.2 (December 2024). Not supported in Firefox as of this writing.
Same-document view transitions, for context, shipped earlier and more broadly: Chrome 111, Safari 18, and Firefox 144 (October 14, 2025, as an Interop 2025 deliverable) - meaning Firefox has same-document support but not yet the cross-document extension this article covers.
| Browser | Same-document | Cross-document |
|---|---|---|
| Chrome / Edge | 111+ | 126+ |
| Safari | 18+ | 18.2+ |
| Firefox | 144+ | Not supported |
These figures are sourced primarily from Google’s own official modern-web-guidance documentation repository, cross-checked against Chrome’s developer blog. MDN’s Browser Compatibility Data, Mozilla’s Firefox Platform Status, and WebKit’s own Feature Status page were not independently fetched as raw primary documents for this reference - that is a disclosed limitation, not an oversight, and readers who need the single most current, continuously-updated figures should check those sources directly rather than treat the table above as anything other than a snapshot as of this writing.
Version-number tables are also a poor substitute for the behavioral-confidence question this reference has tried to answer wherever possible elsewhere: a version number confirms an engine ships the feature, not that every normatively-described edge case behaves identically across engines that ship it. Section 8 already disclosed WebKit’s specific, confirmed divergence on the fragmentation-exclusion check; no equivalent engine-by-engine behavioral audit was performed for the rest of the surface covered in this reference, and none should be assumed from the version table alone. Chromium is, by a wide margin, the best-documented implementation covered here - a direct consequence of it being both the origin engine for this feature and the one with the most publicly accessible source and design-discussion history, not evidence that its behavior is more “correct” than a spec-conformant alternative implementation would be.
Key takeaway: This is a two-of-three-engines feature as of this writing, with the one holdout (Firefox) having already shipped the same-document half - meaning the remaining gap is specifically the navigation-coordination layer this article covers, not the underlying transition mechanism itself.
17. Open Questions
Two-phase capture (Section 9.5) is the most substantial open implementation question: whether starting capture at navigation start rather than response arrival needs a specification change, or is achievable as an internal engine improvement, remains genuinely unresolved. The practical stakes are concrete - every millisecond between navigation start and the current capture point is a millisecond the old page sits frozen for no normatively necessary reason, and closing that gap is the single highest-leverage change available to the implementation as of this writing.
Cross-origin redirect eligibility (issue #11063) asks whether the current blanket restriction - any cross-origin hop in the redirect chain disables the transition - should be relaxed for cases where the final document is still same-origin with the originating one. This sits directly downstream of the security framing in Section 12: relaxing it means reasoning carefully about what a cross-origin intermediary in the chain could infer or influence, not simply loosening a check that turns out to be overly strict.
The view transition layer’s relationship to the CSS top layer is flagged directly inside the specification itself as needing to be folded into the CSS Positioned Layout module, where the top-layer concept is formally defined - currently a cross-specification loose end acknowledged by the editors. Practically, this affects authors combining view transitions with <dialog>, popovers, or fullscreen elements, where the exact painting-order interaction between the top layer and the view transition layer described in Section 6 isn’t yet fully specified in one place.
UA-versus-author transition precedence (issue #8953) is unresolved for platforms where the browser itself already performs a native visual transition during navigation - iOS Safari’s and Chrome’s swipe-back gesture being the cited example - where running an author-defined ViewTransition on top of an already-occurring native transition would be visually incorrect, but suppressing the author’s transition entirely has its own trade-offs. The discussion notes this genuinely varies by platform: some browser/OS combinations already animate the swipe gesture natively, others don’t, meaning a single specified answer has to account for both cases without regressing either one.
A rejected alternative opt-in design is worth noting for context rather than as a currently live question: a proposal to opt in via a page-transition media query, rather than the shipped @view-transition rule, was discussed (issue #8868) and not adopted, but the discussion itself illustrates a real design tension between opt-in-as-a-rule and opt-in-as-a-condition that resurfaces in some of the still-open issues above. The media-query proposal’s specific appeal was compositional - @media (page-transition) or (prefers-reduced-motion) { ... } would have let authors combine transition-state with other media conditions using ordinary CSS logic, something the current dedicated at-rule doesn’t offer as naturally.
Key takeaway: The most consequential open question by far is two-phase capture - it is the one item on this list with an active prototyping effort behind it, rather than a design discussion that may or may not resume.
18. Conclusion
Cross-document view transitions solve a specific, narrow development problem: producing one coherent animated illusion out of two documents that share nothing - no DOM, no script scope, no direct reference to each other - coordinated only through the navigation passing between them.
That framing, introduced in Section 3, explains almost everything that follows it. The lifecycle is two independent phases connected by two events. The rendering pipeline is a deliberately isolated compositing layer, not an extension of either document’s own tree. And the failure modes that dominate real-world reports - duplicate names, BFCache-persisted state, overused render-blocking - are coordination failures between two independent actors, not defects in the animation mechanism itself.
What’s genuinely still in motion is narrower than it might first appear: two-phase capture timing, cross-origin redirect eligibility, and the top-layer/view-transition-layer relationship are the live open items, while the core capture-and-animate model this specification builds on is already stable and shipping in two of three major engines.
More broadly, this feature is a useful case study in a pattern likely to recur as the web platform keeps closing gaps between multi-page and single-page architectures: rather than adding capability to a single document, it coordinates behavior across a boundary - navigation - that the platform has always treated as a hard reset. The mechanisms built to make that coordination reliable without shared state - event-based handoff, render-blocking as a stabilization signal, name-based rather than reference-based element matching - are not incidental implementation choices. They are the direct, necessary consequence of two documents genuinely having nothing in common except the navigation passing between them, and understanding that constraint explains far more of this specification’s shape than any single algorithm does in isolation.
Key takeaways
- A cross-document transition is two
ViewTransitionobjects, one per document, with no shared state between them - everything is coordinated throughpageswapandpagerevealalone.- Activation is a single joint condition (same-origin, visible, user-initiated, redirect-free, mutually opted-in), not several independent preferences - any one failing silently disqualifies the whole transition.
- The render-blocking mechanism is the cross-document substitute for the same-document update callback, and overusing it is the most common self-inflicted performance problem.
- BFCache doesn’t introduce new bugs so much as expose latent ones in transition-name lifecycle management, because it’s the mechanism that makes stale handler-assigned state observable at all.
- Where complete normative algorithm text could not be retrieved, this reference says so explicitly rather than reconstructing it - treat Section 8’s disclosed gaps as an invitation to consult the live specification, not as settled fact.
19. References
Primary Specifications
- CSS View Transitions Module Level 2 (W3C Editor’s Draft) - https://drafts.csswg.org/css-view-transitions-2/
- CSS View Transitions Module Level 1 - https://drafts.csswg.org/css-view-transitions-1/
- HTML Standard -
pageswap,pagereveal, render-blocking mechanism, navigation types - https://html.spec.whatwg.org/multipage/ - Navigation API - https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API
Implementation Sources
- Chromium Blink component tracker -
Blink>ViewTransitions components/viz/README (Transitions subdirectory) - Chromium sourcethird_party/blink/renderer/core/document_transition/andview_transition/- Chromium source
Design Discussions
- w3c/csswg-drafts issues #8804 (original high-level algorithm proposal), #8684 (third-party CSS/navigation-awareness concern), #8783 (navigation descriptor), #8785 / #8790 (event timing), #8868 (media-query opt-in alternative, not adopted), #8953 (UA vs. author transition precedence), #9822 (page-hidden-before-render), #11063 (cross-origin redirect eligibility, open), #9672 (capture ordering), #11071 (capture-exclusion consistency, closed)
- WICG
view-transitionsrepository -cross-doc-explainer.md - W3C TAG design review #851 (cross-document API)
Web Platform Tests
html/browsers/browsing-the-web/history-traversal/pagereveal/tentative/-order-in-new-document-navigation.html,order-in-bfcache-restore.html,order-in-prerender-activation.html
Browser Documentation
- Chrome for Developers - “Cross-document view transitions for multi-page applications”
- Chrome for Developers - “What’s new in view transitions (2025 update)”
- Chrome 127 release notes (iframe support)
- Google
modern-web-guidancerepository (compatibility figures)
Further Reading
- MDN - View Transition API reference (compatibility and API-shape confirmation only, not treated as architectural authority in this reference)
From the team at
We build digital products and explore the modern web standards behind them.