View Transitions API: startViewTransition() Snapshot Trees
- Last updated
- Prerequisites:
- CSS animations and pseudo-elements
- Understanding of DOM updates and rendering lifecycle
- css
- view-transitions
- animation
- snapshot
- dom
- browser-api
Read at your depth
The practical view
document.startViewTransition(() => updateDOM()) takes a snapshot of the current page, runs your callback (which updates the DOM), snapshots the new state, and animates between them. The default animation is a crossfade. You can customize it with CSS pseudo-elements: ::view-transition-old(root) and ::view-transition-new(root). Give elements a view-transition-name to get individual morphing animations between old and new positions. Cross-document transitions work with @view-transition { navigation: auto; } on same-origin navigations. Keep the callback synchronous or return a promise so the browser knows when the new state is ready.
The same idea in other frameworks
React apps wrap route changes with startViewTransition around the setState that swaps the route tree.Angular Router events can trigger startViewTransition around the activated-route component swap.Vue Router's route change handlers can run inside startViewTransition for SPA morphing transitions.SvelteKit's onNavigate callback can wrap page updates with document.startViewTransition for cross-page transitions.Legacy vs modern
JS-driven enter/exit animations vs the View Transitions API
Hand-rolled enter/exit animations require two states and coordination; startViewTransition snapshots both states automatically and crossfades/morphs them.
oldEl.classList.add('exit'); // ❌ OLD: manual exit
setTimeout(() => { swapDOM(); newEl.classList.add('enter'); }, 300);document.startViewTransition(() => { swapDOM(); }); // ✅ NEW: snapshots + morphs automaticallyInterview gotchas
Context
This probes the core mechanism — that the API rasterizes both states into compositor snapshots and animates them, which is different from simply running JS animations on live DOM.
The mechanical answer
Before running the callback, the engine performs a capture pass: it renders the current page and rasterizes it into snapshot images (a root snapshot plus one per element with a view-transition-name). The callback then updates the DOM, and a second capture pass rasterizes the new state. The engine mounts a view-transition pseudo-element tree (::view-transition-old/new) in the top layer and animates these snapshots — old image sliding/crossfading out while the new image animates in. Because the animation plays on captured images, the actual DOM swap is hidden behind them; no blank flash can appear. The live DOM is already the new state underneath, so after the animation the snapshots are removed and the real content shows.
Trap
The naive answer is 'it delays the DOM update until the animation ends'. The DOM updates during the callback; the animation runs over snapshots on top. Another trap: thinking every element animates individually by default — only elements with a view-transition-name get their own snapshot; everything else is part of the root crossfade. Candidates who explain the capture/commit/crossfade phases show real understanding.
Context
This tests the promise-driven lifecycle and error semantics — interviewers want to know the guarantees around the updateCallbackDone, ready, and finished promises.
The mechanical answer
startViewTransition returns an object with three promises: updateCallbackDone (the callback resolved/rejected), ready (snapshots captured and animation about to start), and finished (animation completed or skipped). If the callback throws, updateCallbackDone rejects, the new-state capture is skipped, and the transition is skipped — the DOM change still happened but no snapshots animate (the page just shows the new state). If the callback returns a promise, the browser waits for it before capturing the new state, so async DOM updates (e.g. awaiting data) are supported. A skipped transition fires finished as resolved but with skipTransition() semantics; calling skipTransition() during the animation jumps straight to the final state.
Trap
The naive answer is 'the transition always animates'. If the callback throws, no animation plays. Another trap: awaiting only t.finished while assuming the DOM is updated — the DOM is updated when updateCallbackDone resolves, which can precede finished by the animation duration. Candidates who can enumerate the three promises and their ordering demonstrate real lifecycle mastery.