View Transitions

The browser animates between two states you describe.

← All motions

View Transitions

Every other effect on this site is a property animation you build by hand. This one is different: you tell the browser what the page looks like before and after, and it works out the frames. The catch is the one line everybody forgets — view-transition-name must be unique in the document, so exactly one element can own a name at a time.

trigger: navigation document.startViewTransition() shared element morph

Live demo

The prompt

Build a grid of cards that expands into a detail view, using the View Transitions API for the change.

Structure:
- The list and the detail view are two states of the same container. Only one is visible at a time.
- The list contains a thumbnail per item; the detail view contains a larger media block.
- The thumbnail and the media block are the shared element. Whichever is visible carries view-transition-name: hero.

Behaviour:
- On card click, set the clicked card's thumbnail to view-transition-name: hero, then call document.startViewTransition with a callback that removes the inline name and switches the container into detail mode. CSS then gives the media block the same name, so the browser captures one named element on each side and morphs between them.
- Reversing is the mirror image: the detail media already owns the name, and the callback switches back to the grid and assigns the name to the chosen card.
- Clear any inline view-transition-name after the transition finishes.

Correctness:
- view-transition-name must be unique in the document at every moment. Two elements sharing a name make the transition abort, and the change happens instantly with no error worth reading.
- Set the name on exactly one element per state, never on all the cards.

Fallback:
- If document.startViewTransition does not exist, perform the same state change synchronously. The result is an instant swap, which is a completely acceptable outcome.

Motion design:
- Keep the whole transition between 220 and 320ms. Longer durations make the morph feel like the interface is thinking.
- Customise with ::view-transition-group(hero) for the shared element and keep the default cross-fade for the rest, or disable the root cross-fade if it double-exposes the page.
- Under prefers-reduced-motion: reduce, disable all view-transition animations.

Cross-document:
- For full page navigations between static pages, add @view-transition { navigation: auto } to both stylesheets and give matching elements the same view-transition-name. No JavaScript is involved on the new page.

What actually makes it work

Compatibility and performance

AspectDetail
Same-document APIChrome 111+, Safari 18+, Edge 111+. Firefox 144+. Older Firefox falls back to an instant swap.
Cross-documentSame browsers; requires @view-transition { navigation: auto } on both pages.
CostOne snapshot of the affected area per transition. Cheap for a single element; a full-page morph on a long document rasterises both versions.
During the transitionThe page is not interactive for the duration. Keep it under 300ms or clicks will feel dropped.

One practical note: this is the only effect here that needs a real state change to exist at all. There is nothing to demo without something to transition between, which is why the honest way to evaluate it is on a navigation or an expand, not on a static card.

When not to use it

Don't use it on every link. A 300ms transition on each navigation adds up to seconds across a session, and it delays the content the visitor asked for. Reserve it for changes of context: opening a record, moving between steps.

Don't morph unrelated elements. A card becoming a full-page header looks impressive once and confusing the second time, because the visitor cannot tell what the new screen is.

Don't rely on it for orientation. The transition is decoration; the destination must still be obvious from the resulting layout, including when the animation does not run at all.