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.

Where each symptom originatesWork left to right: most failures happen before any animation runs.Where each symptom originatesCapture oldnames unique?Update DOMcallback resolves?Capture newelement exists?Build pseudotreegroups per nameAnimatekeyframes
Work left to right: most failures happen before any animation runs.

Step-by-step resolution

A repeatable view-transition debugging sessionSlow it, freeze it, then inspect the tree the browser built.A repeatable view-transition debugging session1Open More tools, Animations, and set playback speed to 10%.The transition becomes watchable2Trigger the transition and click pause in the drawer once it appears.The pseudo-element tree stays in the DOM3In Elements, expand html to find ::view-transition and its groups.One group per named element4Select each ::view-transition-old and -new and hover it to highlight the snapshot.Shows exactly what was captured5Read the Styles pane on the group for its generated width, height and transform keyframes.Explains stretching and odd paths6If nothing appears, check the Console and log transition.ready.
Slow it, freeze it, then inspect the tree the browser built.

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.

Symptom, where to look, and the usual causeMost view-transition bugs are diagnosable from the paused pseudo-element tree or the Console.Symptom, where to look, and the usual causeLook inUsual causeNothing animatesConsole, transition.readyDuplicate nameFade instead ofmorphPaused tree: groupsName missing on one sideContent stretchesStyles on -old / -newDefault fill sizingJanky morphPerformance panelSlow update callbackCross-documentdoes nothingBoth pages' CSS, NetworkMissing opt-in or cross-origin
Most view-transition bugs are diagnosable from the paused pseudo-element tree or the Console.

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.