View transition list → detail
Click a card in a list, navigate to its detail page, and watch the image + title smoothly morph into place.
Live demo
Click a card to expand it. The image and title morph smoothly via the View Transitions API.
Mark the morphing pieces
Each shared element gets a unique view-transition-name on both pages. The browser matches names between snapshots to know what to morph.
TIP
For dynamic values like card-${id}-image, prefer binding via :style because UnoCSS can't statically extract template-literal class names.
vue
<button v-for="card in cards" :key="card.id" @click="navigate(card.id)">
<div class="h-32" :style="{
background: `hsl(${card.hue} 70% 60%)`,
viewTransitionName: `card-${card.id}-image`,
}" />
<div class="p-3 bg-white" :style="{ viewTransitionName: `card-${card.id}-title` }">
{{ card.title }}
</div>
</button>vue
<div class="h-64 rounded-lg" :style="{
background: `hsl(${hue} 70% 60%)`,
viewTransitionName: `card-${id}-image`,
}"
/>
<div class="p-4 text-2xl font-semibold" :style="{ viewTransitionName: `card-${id}-title` }">
Card {{ id }}
</div>Trigger the transition
ts
export function startViewTransition(cb: () => void | Promise<void>) {
const doc = document as Document & {
startViewTransition?: (cb: () => void | Promise<void>) => unknown
}
if (typeof doc.startViewTransition === 'function')
doc.startViewTransition(cb)
else
cb()
}ts
import { startViewTransition } from './use-view-transition'
function navigate(id: number) {
startViewTransition(() => router.push(`/card/${id}`))
}Customize the snapshot animations
Use the pseudo-element variants to override the default cross-fade with something more expressive:
html
<div
class="vt-old-[page-title]:animate-slide-out-left
vt-new-[page-title]:animate-slide-in-right"
/>