Debugging View Transitions in DevTools
Part of View Transitions API Implementation in Modern View Transitions & Scroll APIs.
The problem
View transitions are hard to debug by eye. The whole effect usually lasts under half a second, it runs on pseudo-elements that do not appear in the normal DOM tree, and when something goes wrong the browser often recovers silently: the transition is skipped, the page updates instantly and there is nothing on screen to inspect.
The common reports are a transition that does nothing, a shared element that cross-fades instead of morphing, a morph that stretches content, and a cross-document transition that works locally and not in production. Each has a specific DevTools signature.
Root cause analysis: a short-lived tree of snapshots
When document.startViewTransition() runs, or a cross-document navigation opts in, the browser captures the current state of every element with a view-transition-name, runs the DOM update, captures the new state, and then builds a temporary pseudo-element tree on the root:
::view-transition— the overlay covering the page::view-transition-group(name)— one per name, animating position and size from old to new::view-transition-image-pair(name)— isolates the blending of the two snapshots::view-transition-old(name)and::view-transition-new(name)— the captured images, cross-faded by default
The tree exists only while the animations run. Every failure is a failure at one of those stages: capture (the element was not named, or two were given the same name), update (the callback threw or took too long), or animation (the default or custom keyframes produced something unexpected). DevTools can freeze the tree mid-animation, which turns an invisible half-second into something you can inspect like any other element.
Step-by-step resolution
Diagnosing a transition that does nothing
If the Animations drawer records nothing, the transition was skipped or never started. The most common cause is a duplicate name: two rendered elements with the same view-transition-name at capture time. The browser aborts the transition, updates the DOM without animation and logs an error in the Console mentioning the duplicate name. Lists that assign names in a loop are the usual source.
A transition can also be skipped because the update callback rejected, because the document was hidden, or because another transition started first. Those surface only as a rejected ready promise, so log it:
const transition = document.startViewTransition(() => updateTheDom());
transition.ready.catch((err) => console.warn('View transition skipped:', err.name, err.message));
transition.finished.finally(() => console.debug('View transition finished'));
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
}
Diagnosing a cross-fade that should be a morph
When a shared element fades between two positions instead of moving, the paused tree shows why: there are two groups instead of one, or one group where either -old or -new is missing. A missing -old means the name was not on the element at capture time — often because the name is applied by a class added in the update callback. A missing -new means the element with that name did not exist after the update, frequently because it renders asynchronously after the callback resolves. Make the callback wait for the new content, as described in morphing list to detail with shared elements.
Diagnosing stretched content
A group animates its width and height from the old box to the new one, and by default the snapshots inside stretch to fill it. Text in a card that changes aspect ratio looks smeared. Select the ::view-transition-old in the paused tree and you will see it filling the group. The fix is to keep the snapshots at their natural size and let the group clip them:
::view-transition-old(card),
::view-transition-new(card) {
block-size: 100%;
inline-size: auto;
object-fit: none; /* no stretching; the group's overflow clips */
overflow: clip;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(card) { animation-duration: 0s; }
}
Rendering Impact: composite for the snapshot cross-fade and group transform; the group’s width and height animation may run on the main thread. Snapshots are textures, so a transition with many large named elements allocates a texture per old and new image.
Cross-document transitions
A multi-page transition needs @view-transition { navigation: auto; } on both the outgoing and the incoming page, and the navigation must be same-origin. Check the incoming page’s stylesheet in the Sources panel, not only the page you started on. Redirects across origins cancel the transition.
Timing problems are the other half. The browser captures the new page as soon as it can render, so a hero image or web font that arrives later is missing from the new snapshot. Record a Performance trace across the navigation and look for the first render of the new document. To hold rendering until a critical element is parsed, the new page can use a render-blocking <link rel="expect"> pointing at that element’s id — sparingly, because it delays first paint for every visitor. The pageswap and pagereveal events expose the transition object on each side for logging, which is the cross-document equivalent of the ready promise.
Verification checklist
Constraints and trade-offs
- Pausing in the Animations drawer freezes the visual state but not script; update callbacks with timeouts can still fire.
- Very short transitions can finish before you can pause them; slowing playback first is essential.
- Each named element costs a snapshot texture per side; debugging a list where every item is named can itself be slow on low-memory devices.
- Cross-document debugging across a real navigation loses Console history unless Preserve log is enabled.
- Behaviour of snapshot sizing and group animations has evolved across engine versions; confirm fixes in each target browser.
Frequently asked questions
Where do the ::view-transition pseudo-elements appear in DevTools?
Under the html element in the Elements panel, but only while the transition is running. Slow playback in the Animations drawer, trigger the transition and pause it to keep the tree available for inspection.
Why does my view transition get skipped with no visible error?
The most common reason is two rendered elements sharing a view-transition-name, which logs a Console error. Other causes — a rejected update callback, a hidden document or an interrupted transition — only show as a rejected transition.ready promise, so log it.
Why does text look stretched during the transition?
The group animates width and height, and by default the old and new snapshots are sized to fill it. Set object-fit: none and natural sizing on the snapshots so the group clips rather than stretches them.
Why does my cross-document transition work locally but not in production?
Check that both pages include the @view-transition opt-in, that no redirect crosses origins, and that caching or a CDN is not serving an older stylesheet on one side.
Related
- View Transitions API Implementation — the parent topic
- Implementing Cross-Document View Transitions in SPAs — the opt-in and navigation setup being debugged
- Handling View Transitions with Reduced Motion — verifying the reduced branch