View Transition
The browser-native View Transitions API that animates between DOM states or page navigations using a screenshot-based crossfade, with no JavaScript animation library needed.
Plain English
Before the View Transitions API, creating a smooth animated transition between two pages or two UI states required either a JavaScript animation library or complex manual DOM manipulation — clone the old state, animate both, swap, clean up. The View Transitions API delegates all of that to the browser in two lines: document.startViewTransition(() => updateTheDom()). The browser takes a snapshot of the current state, applies the DOM update, then crossfades between the old and new snapshots — by default with a 300ms opacity crossfade. The real power comes from named elements: assign view-transition-name: card-hero to an element in both states and the browser will automatically animate that element from its old position and size to its new one, producing the "shared element transition" effect (the card flying from the list into the detail page) that previously required significant engineering effort.
Technical
Same-document: document.startViewTransition(async () => { await updateDOM(); }). Multi-page (MPA): add @view-transition { navigation: auto; } in CSS (Chrome 126+). Named elements: view-transition-name: hero-image on matching elements in old/new state — browser auto-animates position/size. Custom animation: ::view-transition-old(root) and ::view-transition-new(root) are pseudo-elements you can target with @keyframes. SPA frameworks: Next.js supports MPAs via the App Router's soft navigation + unstable_ViewTransition (experimental); React Router 7 has built-in support. Browser support: Chrome 111+ (same-doc), Chrome 126+ (MPA). Firefox and Safari: in development as of mid-2025 — always guard with if (!document.startViewTransition) { fallback(); }.
Live Demo
View Transition API
Simulates the View Transitions API — the shared card element morphs between its position in the list and the hero position on the detail page.
All Terms
The View Transitions API animates shared elements between page states — no JS animation library needed.
Usage
✓ Good usage
A card grid where clicking a card triggers a view transition — the card's image expands in place to fill the detail page header using view-transition-name on the image, producing a native-app-quality shared element animation with 4 lines of code.
✗ Bad usage
Wrapping every small DOM mutation (toggling a class, updating a counter) in startViewTransition() — the API adds a compositing frame for every call, and overuse on frequent updates causes dropped frames rather than smoothness.
Recommended values
- Default crossfade duration: 300ms (override via ::view-transition-group)
- Named element transitions: view-transition-name must be unique per frame
- Custom easing: animation-timing-function on ::view-transition-old/new
- Exit animation: ::view-transition-old(root) { animation: slideOut 200ms }
- Entry animation: ::view-transition-new(root) { animation: slideIn 300ms }
- MPA opt-in: @view-transition { navigation: auto; } in global CSS
Common mistakes
- Non-unique view-transition-name values in the same frame — two elements with the same name in the same DOM state cause the transition to silently fail or produce visual artifacts.
- No feature detection fallback — Safari and Firefox do not support the API yet; unwrapped calls throw a TypeError and break the navigation entirely.
- Forgetting that startViewTransition() is async — DOM updates that happen before the promise resolves are not captured in the snapshot, resulting in a flash of unstyled content during the transition.
AI Prompt Example
Copy this into Claude, Cursor, Bolt, or v0.
Add view transitions to the product listing → detail navigation. In the listing, give each product card image view-transition-name: product-image-{id}. In the detail page, give the hero image the same view-transition-name. Wrap the router push in document.startViewTransition(() => router.push(url)) with a feature check: if (!document.startViewTransition) { router.push(url); return; }. Add a custom slide-and-fade: ::view-transition-old(root) animates out left, ::view-transition-new(root) slides in from right, both at 280ms cubic-bezier(0.4, 0, 0.2, 1).