Single‑file Alpine.js + Tailwind tutorial that teaches the View Transitions API with runnable demos, step playback, and a Terminal‑Neon aesthetic.
An interactive tutorial and playground for the View Transitions API. It explains SPA (same‑document) transitions, shared‑element transitions, optional cross‑document (MPA) navigation, and how to build robust fallbacks that respect accessibility and performance.
- Stack: Plain HTML + Alpine.js (CDN) + Tailwind (CDN) + Fira Code
- Design: Terminal‑Neon cards, glow accents, dark theme
- File:
index.html(no build step)
- Modern Chromium‑based browsers; partial features land in others over time
- Feature detection is mandatory:
const supported = 'startViewTransition' in document;
- Prefer reduced motion respect:
const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
- startViewTransition(cb): Snapshot → DOM update in
cb→ browser creates transition pseudo‑elements → animate per CSS. - Promises:
transition.readyresolves when snapshots & pseudo‑elements are ready;transition.finishedresolves after animation completes. - Shared elements: Link visual continuity by giving the same
view-transition-nameto old/new DOM counterparts. - Cross‑document: Some browsers support declarative navigation transitions; provide graceful fallback.
- SPA Route Fade – swap section content via Alpine state inside
startViewTransition. - Shared Element (Grid → Detail) – card thumbnail travels/expands into detail.
- MPA/Cross‑Doc (Optional) – pattern + fallback when unsupported.
- Reduced Motion – skip animations, still log phases for learning.
Each demo has:
- Controls: Prev / Next / Auto / Reset
- A pipeline strip: Snapshot → Update → Animate → Finish with active phase highlight
- Live code with current line highlight
- Console panel logging
ready/finishedtimestamps
Include Tailwind and Alpine via CDN:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>Run by simply opening index.html in a supported browser.
Minimal pattern you’ll see in the demos:
document.startViewTransition(() => {
// mutate DOM synchronously here (swap route, toggle view, etc.)
});Then style the pseudo‑elements in CSS (see Cheat‑Sheet below).
- Respect
prefers-reduced-motion; disable or drastically soften transitions. - Maintain focus order on route changes.
- Avoid disorienting large‑scale moves; prefer small, meaningful continuity.
- Snapshot cost scales with the painted area. Prefer shared‑element transitions over whole‑page fades.
- Avoid heavy filters (blur, drop‑shadow) on large regions.
- Constrain layout shifts; prefer transforms over geometry reflow.
- Lazy assets (images) may pop in late—consider placeholders or preloading.
- Outline participating elements (utility toggles in page)
- Temporarily slow animations with longer durations
- Use devtools to inspect layers/paint (Rendering/Performance panels)
- Missing
view-transition-nameon either side → no shared animation - Reparenting DOM without maintaining the named element → transition breaks
- Animating huge containers → jank; animate smaller focus elements instead
- Mutations outside the
startViewTransitioncallback → won’t be captured
- Fade route: whole‑root fade with soft easing
- Shared thumbnail → detail: scale & translate via image‑pair
- List reorder: per‑row shared names to preserve continuity
- Panel slide‑in: constrain to sub‑tree and animate transform
- Add “staging” via
transition.updateCallback(cb)patterns - Add user‑editable CSS sandbox per demo
- Time travel scrubber for phases
- MIT. Credit MDN & Chrome DevRel articles for references; plus internal design system origins.
/* Root pseudo‑elements */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 300ms;
animation-timing-function: cubic-bezier(.2,.8,.2,1);
}
::view-transition-old(root) { animation-name: vt-fade-out; }
::view-transition-new(root) { animation-name: vt-fade-in; }
@keyframes vt-fade-out { from { opacity: 1 } to { opacity: 0 } }
@keyframes vt-fade-in { from { opacity: 0 } to { opacity: 1 } }Use:
document.startViewTransition(() => swapRoute());/* Give both old and new elements the SAME name */
.card-thumb[data-id] { view-transition-name: card-attr; }
.detail-hero { view-transition-name: card-attr; }
/* Optional: tune the pair */
::view-transition-group(card-attr) {
/* grouping layer for the named element */
}
::view-transition-image-pair(card-attr) {
/* controls the interpolated snapshot pair */
isolation: isolate; /* reduce blending surprises */
}DOM idea:
<div class="card-thumb" :data-id="id" style="view-transition-name: card-{{id}}"></div>
<!-- In detail view -->
<div class="detail-hero" style="view-transition-name: card-{{id}}"></div>/* Limit to a container by naming only that subtree's key element */
.sidebar-panel { view-transition-name: sidebar; }
::view-transition-old(sidebar) { animation: slide-out 250ms both; }
::view-transition-new(sidebar) { animation: slide-in 250ms both; }
@keyframes slide-out { from { transform: translateX(0) } to { transform: translateX(-8%) } }
@keyframes slide-in { from { transform: translateX(8%) } to { transform: translateX(0) } }@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root),
::view-transition-group(*),
::view-transition-image-pair(*) {
animation: none !important;
}
}JS toggle (optional):
const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) {
// Skip startViewTransition entirely, or drastically shorten durations
}:root {
--vt-dur-short: 180ms;
--vt-dur-base: 300ms;
--vt-ease: cubic-bezier(.2,.8,.2,1);
}
::view-transition-old(root),
::view-transition-new(root) { animation-duration: var(--vt-dur-base); animation-timing-function: var(--vt-ease); }/* Visualize participants */
::view-transition-old(*),
::view-transition-new(*) {
outline: 1px dashed rgba(0, 255, 255, .35);
}<script>
const supported = 'startViewTransition' in document;
document.documentElement.classList.toggle('vt-supported', supported);
</script>
<style>
.vt-supported .vt-fallback { display: none; }
.vt-fallback { /* show a CSS/JS fallback notice */ }
</style>const t = document.startViewTransition(() => {
// Phase 1 DOM change
});
// Optional staged update after ready
await t.ready;
// do additional class toggles or lazy decorations if needed
await t.finished;- Pipeline strip CSS (active phase glow)
- Console panel styles (monospace, scrollback)
- Card grid (shared element demo)
- Controls (Prev/Next/Auto)
Keep the above blocks co‑located in
index.htmlwith clear comments so learners can see exactly how the pseudo‑elements and names drive the animation behavior.