A thumbnail grows into a header image. A title slides up and gets bigger. It looks like the browser is morphing one element into another.
It isn't. Slow it down, turn on x-ray, and watch what's really moving. Try it below.
Two pictures in a moving box
When you call document.startViewTransition(), the browser does four things in order.
- Capture the old state A picture of each named element
- Run your update Rendering is paused
- Capture the new state Live, not a picture
- Animate a pseudo-element tree Then remove it
First, it takes a snapshot of every element that has a view-transition-name, plus the whole page, which gets the name root by default. Then it calls your function, which changes the DOM however it likes. Then it looks at the new DOM and finds the elements with the same names.
For each name, it now has an old picture and a new view. It builds a tree of pseudo-elements on top of the page, animates it, and removes it when the animation ends.
thumb.style.viewTransitionName = "vt-thumb";
document.startViewTransition(() => {
showDetail(album); // the new DOM has its own element named vt-thumb
});
That's all the demo does. Two elements that share a name are treated as the same thing, even though they're different DOM nodes.
The tree the browser builds
This is the structure that exists for the length of the transition. Switch direction to see how it changes when an element only exists on one side.
Each part has one job, according to the spec's default style sheet.
::view-transitionis an overlay that covers the page.::view-transition-group(name)is a box that starts at the old element's size and position, and animates to the new one's. It animateswidth,heightand atransform.::view-transition-image-pair(name)holds the two images and isolates their blending, so the crossfade doesn't mix with the page beneath.::view-transition-old(name)and::view-transition-new(name)are the two images. Old fades from 1 to 0, new fades from 0 to 1, and both usemix-blend-mode: plus-lighterso the overlap doesn't dip in brightness halfway.
The default duration is 0.25s, set on the group. That's why slow motion in the demo is one rule:
::view-transition-group(*) {
animation-duration: 2.5s;
}
If an element only exists in the new state, its pair has only a ::view-transition-new. It fades in, with nothing to crossfade from. If it only exists in the old state, it fades out.
Why text stretches and images spill
So a "morph" is a moving, resizing box containing a crossfade of two pictures. Every odd thing you'll see comes from that.
The old image is a picture, not live content. When the group doubles in width, the picture doubles too. The text inside it scales like a photo of text. It gets bigger and a little soft, and it doesn't rewrap. That's why a title that changes size looks fine, but a paragraph that changes width looks wrong. Its lines would wrap differently at the new width, and a picture can't rewrap.
By default, each image fills the group's width and keeps its own aspect ratio. In the demo, the square thumbnail grows into a wide banner. The old square image is scaled to the banner's width, so it becomes far taller than the box and spills out underneath as it fades. The new wide image starts as a thin strip inside a square box.
The fix is to make both images fill the box and crop. The overflow: clip matters: without it, the cropped-away part still paints outside the box.
::view-transition-old(vt-thumb),
::view-transition-new(vt-thumb) {
height: 100%;
overflow: clip;
object-fit: cover;
}
Turn on "Fix aspect ratio" in the demo to see it. The images are treated as replaced content, like an <img>, so object-fit works on them.
For text, give the element width: fit-content. Otherwise a heading in a full-width block captures as a full-width picture with lots of empty space, and the box animates that space too. The demo's titles use it.
Rendering is paused while you update
Between the first snapshot and the new state, the browser stops drawing the page. Your update callback can return a promise, and the transition waits for it. The whole time, the user sees a frozen frame.
So keep the update fast. Fetch data before you call startViewTransition(), not inside it. If the callback takes too long, the browser gives up after an implementation-defined timeout and skips the animation.
Names have one more rule. Each view-transition-name has to be unique on the page at the moment of capture. If two elements share a name, the browser skips the whole transition. That's why the demo names only the clicked album's thumbnail, not all of them. When you do want many elements to share one style, give each a unique name and a shared view-transition-class, then style ::view-transition-group(*.card).
Across pages, with no JavaScript
Everything above also works between two separate HTML pages. Add this to the CSS of both:
@view-transition {
navigation: auto;
}
On a same-origin navigation, the browser snapshots the old page, loads the new one, and runs the same tree between them. The navigation has to be a normal link click or a back and forward, not a URL typed into the address bar. Two events, pageswap on the old page and pagereveal on the new one, let you set names or types at the last moment, such as naming only the thumbnail that was clicked.
Types let one stylesheet handle different transitions. Pass types: ["forward"] to startViewTransition(), or list them in the @view-transition rule, then match them with :active-view-transition-type(forward).
Support as of November 2026, from caniuse and MDN's compatibility data:
That last point is the nice part. A view transition is an enhancement by design. If a browser can't do it, the page still changes, just without the motion.
Reduced motion
A box flying across the screen is the kind of motion that bothers people with vestibular disorders. Wrap your transition styles in @media (prefers-reduced-motion: no-preference), or check the media query before calling startViewTransition(), as the demo does.
That doesn't have to mean no transition at all. A short crossfade with no movement still tells people the view changed, without anything travelling across their screen.
Once you know the morph is two pictures in a moving box, you stop fighting the browser. You decide which things deserve a name, give them boxes that keep their shape, and let everything else crossfade.