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.
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
- Names must be unique. The API captures one snapshot per name. Two elements claiming
heroat the same moment is invalid, and the browser drops the transition silently — the state still changes, so nothing looks broken, which is why this defect survives review. - The callback is synchronous state, not animation. Everything inside the
startViewTransitioncallback is a normal DOM change. The browser captures the old state before it runs and the new state after. You are not animating anything yourself. - Hand the name over, don't duplicate it. The card owns
herobefore the callback; the detail media owns it after. The handover inside the callback is the entire mechanism. - Cross-fade can double-expose. The default is a fade of the whole page plus the morph. On text-heavy layouts that reads as a ghost. Either shorten the root duration or set the root animation to none so only the shared element moves.
- It degrades to nothing. Without support the callback still runs, just without a snapshot animation. That is the right failure mode for this API and the reason it is safe to adopt early.
Compatibility and performance
| Aspect | Detail |
|---|---|
| Same-document API | Chrome 111+, Safari 18+, Edge 111+. Firefox 144+. Older Firefox falls back to an instant swap. |
| Cross-document | Same browsers; requires @view-transition { navigation: auto } on both pages. |
| Cost | One 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 transition | The 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.