You Might Also Like
View Transitions API Page Navigation — Free document.startViewTransition Demo
View Transitions API Page Navigation · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
View Transitions API Page Navigation — Native Cross-Fades With No Animation Library

This is a genuine SPA-style navigation: clicking a row in a list swaps to a detail view for that item, and back again — the kind of state swap you'd otherwise reach for a router or a manual FLIP animation to make feel smooth. Instead it uses the real document.startViewTransition() browser API to let the browser itself handle the cross-fade.
The real API: startViewTransition's callback
document.startViewTransition(callback) takes a synchronous callback that performs a DOM mutation — here, hiding the list view and showing the detail view (or the reverse). Before running the callback, the browser captures a snapshot of the current DOM state; after the callback runs, it captures the new state; then it automatically cross-fades between old and new pixels using the View Transitions pseudo-element tree, entirely via compositor-driven CSS animations the browser generates for you.
Named elements morph, not just fade
The clicked row's avatar circle gets view-transition-name: vtn-avatar-active applied (via the .vt-active class) right inside the same callback that swaps views. Because both the list avatar and the detail-view avatar share that transition name across the before/after snapshots, the browser treats them as *one* continuous element and morphs its size and position between the two layouts, rather than cross-fading two unrelated circles in place. Removing the class on the way back to the list releases the name so a second click can reuse it. This is a genuinely different interaction than a scroll-driven gallery reveal — this is a discrete state swap between two named "pages" inside one document, not motion tied to scroll position.
Honest fallback: instant swap, no error
document.startViewTransition doesn't exist in every browser (older Firefox and Safari releases lack it). The code checks typeof document.startViewTransition === 'function' up front and, when absent, calls the exact same swap() function directly instead of wrapping it — so the list-to-detail navigation still works perfectly, just without the cross-fade. A visible note explains the fallback is active rather than leaving the user to wonder why nothing animated. Pair this with an accordion FAQ for a broader "native browser transitions" showcase page.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly what document.startViewTransition() captures before and after its callback runs, and why giving two elements across those snapshots the same view-transition-name value causes the browser to morph one into the other instead of cross-fading two separate elements in place. It's also worth asking why this snippet's fallback calls the same swap() function directly rather than duplicating the DOM-mutation logic for the unsupported path, and what would happen if a framework's state update inside the callback resolved asynchronously instead of synchronously. For extensions, ask it to add a third "settings" view with its own transition, use CSS ::view-transition-old and ::view-transition-new pseudo-elements to customize the easing and duration beyond the browser default, or wire this pattern into a real client-side router. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "view transitions page navigation" demo in plain HTML, CSS, and JavaScript using the real browser document.startViewTransition() API — no libraries or router.
Requirements:
- Two in-page "views" inside one container: a list view (e.g. an inbox with a few clickable rows, each showing an avatar, name, and subject) and a detail view (hidden by default) showing the full content of whichever row was clicked, plus a "Back" button.
- A single swap() function per direction that performs the actual DOM mutation (hiding one view, showing the other, and populating the detail view's fields from the clicked row's data). Clicking a row should call this function; clicking Back should call the reverse.
- CRITICAL: feature-detect support with typeof document.startViewTransition === 'function'. When supported, call document.startViewTransition(swapFunction) so the browser automatically snapshots the before/after DOM states and cross-fades between them. When NOT supported (this must be handled explicitly, not left to throw), call the exact same swapFunction directly with no wrapping, so the view still switches correctly — just instantly, without a cross-fade — and show a small visible note in the UI explaining that the browser lacks View Transitions support.
- Give the clicked row's avatar element a shared CSS view-transition-name (applied via a class added inside the same swap callback) that matches the corresponding avatar element in the detail view, so supporting browsers morph the avatar's size and position between the two layouts instead of just cross-fading two independent circles. Remove the class when navigating back so the transition name is free to be reused by a different row's avatar on the next click.
- Make sure the interaction is a discrete list/detail state swap triggered by clicks — not tied to scroll position or an automatic gallery cycle — since that's the use case this snippet is meant to demonstrate.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA three-item inbox list renders.
- 2Click any rowThe view cross-fades to a detail screen for that message.
- 3Watch the avatar morphThe same named element grows into the detail header.
- 4Click "Back to inbox"The transition reverses smoothly back to the list.
- 5Unsupported browser?A note explains the swap is instant instead, with no error.
- 6Swap in your own dataReplace the row dataset attributes with real content.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
This snippet performs a discrete state swap triggered by a click -- one named "view" (the list) is replaced by another (the detail screen) inside the same document, with no dependency on scroll position at all. A scroll-driven view transition effect instead ties its animation progress to how far the user has scrolled. Both can use the View Transitions API, but the trigger and interaction model are entirely different.
It accepts a callback that performs a DOM mutation. Before invoking the callback, the browser captures a screenshot-like snapshot of the current DOM; after the callback finishes, it captures the new state; then it automatically generates and plays a cross-fade (and, for elements sharing a view-transition-name, a morph) between the two snapshots using compositor-driven pseudo-elements it creates internally -- no manual FLIP measurement or animation library required.
The clicked row's avatar circle gets the CSS property view-transition-name: vtn-avatar-active applied via a class, inside the same callback that swaps the views. Because the avatar element in the detail view shares that same transition name in the "after" snapshot, the browser recognizes them as one continuous element across the transition and interpolates its size and position, producing a morph rather than a plain cross-fade of two separate circles.
The code checks typeof document.startViewTransition === 'function' before calling it. If it's missing (older Firefox and Safari releases, for example), the exact same swap() function that performs the DOM mutation is called directly instead of being wrapped in startViewTransition -- so the list-to-detail navigation still works correctly, just as an instant swap with no cross-fade, and a note in the UI explains why.
Wrap your state-updating function (e.g. the setState call that swaps which view renders) in a check for document.startViewTransition: if present, call it as document.startViewTransition(() => { /* trigger the state update, then flush synchronously */ }), noting that framework state updates are often asynchronous, so you may need flushSync (React) or a synchronous DOM update inside the callback for the snapshot timing to work correctly. If unsupported, just call the state update directly.