Shots in this scene
The Snapshot Model
In one line
document.startViewTransition(update) photographs the page, runs your update while the screen shows the photograph, photographs the result, then animates between the two pictures. The default is a 250 ms cross-fade of the whole page.
Think of it like this
A magician's assistant stands in front of a photograph of the stage while the crew changes the set behind it. When the set is ready the photograph is lifted away. The audience sees a smooth hand-over from the old picture to the new one, never the change itself.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- snapshot
- A bitmap picture of the page or of one named element. You cannot click it.
- pseudo-element tree
- The temporary
::view-transitionelements the browser builds for the animation. - update callback
- The function you pass in. It changes the DOM; the new snapshot comes after it.
- root
- The default name for everything with no name of its own: the page itself.
Step by step
01Capture the old page
The browser photographs the page as it is now. Nothing has moved.
02Run your update with rendering paused
The screen keeps showing the old picture. A slow update means a frozen page.
03Capture the new page and build the tree
Slow the first example to 0.1× and press Swap: html::view-transition-group(root), new(root) and old(root) tracks appear, 250 ms each. The card has no track of its own.
04Animate, then give the page back
When finished fulfills the pseudo-elements are removed and the real page is clickable again.
Try it yourself
- 1
Make the update slow
Change the update to
async () => { await new Promise((r) => setTimeout(r, 1000)); card.classList.toggle("b"); }. The page freezes for about a second, then the cross-fade runs. The timeline cannot step through the wait: its clock moves with frames, and frames are paused.
Code & diagrams
Try it: interactive
The four phases and the pseudo-element tree
Step through capture, update, capture and animate, then scrub to watch the old picture fade out and the new one fade in.
Press Pause, then Swap, then Step: the tracks appear. The first line is the feature check.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
startViewTransition is a function
update ran
updateCallbackDone
ready
finishedEach line settles all three promises. A throwing update rejects all three with its error, and the DOM change made before the throw stays. The last line is the fallback: no API, so run the update.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
fine | updateCallbackDone fulfilled | ready fulfilled | finished fulfilled
throws | updateCallbackDone rejected Error: boom | ready rejected Error: boom | finished rejected Error: boom
text after the throw: half-done
fallback returns: undefined, text: fallbackBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call the API with no guard in a browser without it
Add document.startViewTransition = undefined; as the first line, remove the typeof line and call document.startViewTransition(() => card.classList.toggle("b"));.
Myth vs fact
Myth
A view transition animates the elements in my DOM.
Fact
It animates pictures of them. The real elements are already in their final place while the pictures move, which is why you cannot click a moving thing and why stretched text looks soft (Topic 11.2).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Snapshots are bitmaps held in memory for every named element: name what must move and leave the rest to
root. - ▸
finishedrejects only if the update throws or rejects. A skipped or replaced transition still fulfills it.readyis the promise that rejects for skips (Topic 11.5).
Remember this
- 1
The problem this solves: the DOM changes in one frame. To animate old to new you used to keep both versions alive, place them by hand and clean up: hard for a reordered list, impossible for a removed element, very hard across two pages. A view transition keeps the old version as a picture, so you only write the change.
- 2
The three steps. You call
document.startViewTransition(update). (1) The browser captures the old state. (2) It runs yourupdatewith rendering paused: the screen keeps showing the old picture. (3) It captures the new state, builds a tree of pseudo-elements holding both pictures, and animates. A snapshot is a bitmap, not live DOM: it ignores clicks, and a video in it is frozen. - 3
The default is a cross-fade of the whole page:
::view-transition-old(root)fades out while::view-transition-new(root)fades in over 250 ms, blended withplus-lighterso the middle does not dip. Measured in Chromium 153: five tracks, all 250 ms. The tree covers the page while it runs:document.elementFromPointover a button returnedHTML, thenBUTTONafterwards. - 4
Three promises, in this order:
updateCallbackDone(your update's promise fulfilled),ready(the tree exists, the animation is about to start),finished(the animation is over, the page is live again). - 5
The page is frozen while your update waits. Measured:
requestAnimationFrameproduced one frame in 0.9 s while an update waited 1 s. After about 4 secondsreadyrejects with aTimeoutError(the DOM change still happens,finishedfulfills; measured with a 5 s update). Fetch data before you callstartViewTransition, not inside it. - 6
It is an enhancement. Same-document transitions work in about 92.9% of browsers (Chrome 111, Safari 18, Firefox 144; caniuse, 2026-10-19). Elsewhere the call throws a
TypeErrorand your change never happens, so every call is guarded:if (!document.startViewTransition) { update(); return; }.
Explain it without notes
Walk through what the browser does between startViewTransition(update) and the animation starting, and say where your code runs.
Why must every call have a fallback, and what goes wrong if the update does await fetch(url)?
Practice
Write withTransition(update): a transition when supported, plain otherwise, no unhandled rejection for a skipped one.
Make a card toggle cross-fade and log done only when the animation has finished.
Done when you can
You can list the three steps and say where your update runs
You can read the default tracks and name the three promises
You can guard every call and keep updates short