View Transition API Carousel — HTML CSS JS Snippet
View Transition API Carousel · Carousels · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="vtc-wrap">
<div class="vtc-stage" id="vtcStage">
<div class="vtc-slide" style="background:linear-gradient(160deg,#6366f1,#4338ca)"><span>🎧</span><h3>Headphones</h3></div>
</div>
<div class="vtc-controls">
<button class="vtc-btn" id="vtcPrev" aria-label="Previous">‹</button>
<div class="vtc-dots" id="vtcDots"></div>
<button class="vtc-btn" id="vtcNext" aria-label="Next">›</button>
</div>
<p class="vtc-support" id="vtcSupport"></p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f6f7f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.vtc-wrap{display:flex;flex-direction:column;align-items:center;gap:18px}
.vtc-stage{width:220px;height:220px}
.vtc-slide{width:100%;height:100%;border-radius:20px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;box-shadow:0 16px 36px rgba(15,23,42,.2);view-transition-name:vtc-active-slide}
.vtc-slide span{font-size:46px}
.vtc-slide h3{color:#fff;font-size:16px;font-weight:800}
.vtc-controls{display:flex;align-items:center;gap:16px}
.vtc-btn{width:38px;height:38px;border-radius:50%;background:#fff;border:1.5px solid #e3e5ea;color:#4b5563;font-size:19px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color .15s,color .15s}
.vtc-btn:hover{border-color:#6366f1;color:#6366f1}
.vtc-dots{display:flex;gap:7px}
.vtc-dot{width:7px;height:7px;border-radius:50%;background:#d1d5db;border:none;cursor:pointer;transition:background .2s,width .2s}
.vtc-dot.active{background:#6366f1;width:20px;border-radius:4px}
.vtc-support{font-size:11px;color:#9ca3af}var ITEMS = [
{ icon: '🎧', title: 'Headphones', bg: '#6366f1,#4338ca' },
{ icon: '📷', title: 'Camera', bg: '#ec4899,#9d174d' },
{ icon: '⌚', title: 'Watch', bg: '#0ea5e9,#0369a1' },
{ icon: '🎮', title: 'Console', bg: '#10b981,#047857' },
];
var stage = document.getElementById('vtcStage');
var dotsWrap = document.getElementById('vtcDots');
var supportNote = document.getElementById('vtcSupport');
var current = 0;
var supported = typeof document.startViewTransition === 'function';
supportNote.textContent = supported
? 'Your browser supports the View Transitions API — real morph animation below.'
: 'Your browser lacks View Transitions API support — falls back to an instant swap.';
ITEMS.forEach(function (it, i) {
var d = document.createElement('button');
d.className = 'vtc-dot';
d.setAttribute('aria-label', 'Go to slide ' + (i + 1));
d.addEventListener('click', function () { goTo(i); });
dotsWrap.appendChild(d);
});
var dots = document.querySelectorAll('.vtc-dot');
function paint() {
var it = ITEMS[current];
stage.innerHTML = '<div class="vtc-slide" style="background:linear-gradient(160deg,' + it.bg + ')"><span>' + it.icon + '</span><h3>' + it.title + '</h3></div>';
dots.forEach(function (d, i) { d.classList.toggle('active', i === current); });
}
function update(newIndex) {
current = newIndex;
// document.startViewTransition captures a snapshot of the DOM before AND
// after the callback runs, then cross-fades/morphs between them for any
// element carrying a view-transition-name — here, the slide itself. No
// manual keyframes are written for this transition at all; the browser
// interpolates position, size, and appearance between the two snapshots.
if (supported) {
document.startViewTransition(function () { paint(); });
} else {
paint();
}
}
function next() { update((current + 1) % ITEMS.length); }
function prev() { update((current - 1 + ITEMS.length) % ITEMS.length); }
function goTo(i) { update(i); }
document.getElementById('vtcNext').addEventListener('click', next);
document.getElementById('vtcPrev').addEventListener('click', prev);
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') next();
else if (e.key === 'ArrowLeft') prev();
});
paint();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
View Transition API Carousel — The Browser Animates the Diff, Not You

Every other carousel in this library animates a transition by hand — a transform, an opacity, a keyframe. This one hands that job to the browser itself: document.startViewTransition(callback) snapshots the DOM as it is right now, runs the callback (which just replaces the slide's HTML — no animation code inside it at all), snapshots the DOM again afterward, and then automatically cross-fades and morphs between the two snapshots for any element carrying a matching view-transition-name.
One CSS property is the entire animation setup
The *only* animation-related code in this snippet is view-transition-name: vtc-active-slide on .vtc-slide in the CSS. That single declaration tells the browser "treat this element as a named subject to animate between states" — there's no transition, no @keyframes, no JS-driven transform anywhere in the slide-change logic. paint() just tears down the old slide's markup and writes in the new one synchronously; the browser handles everything about *how* that change appears.
Feature detection, not a hard dependency
Because the View Transitions API isn't universal yet, every call is guarded: typeof document.startViewTransition === 'function'. When it's missing, update() just calls paint() directly — the carousel still works, it simply swaps instantly instead of animating. That's a deliberate progressive-enhancement pattern: the feature adds polish where supported and never breaks functionality where it isn't.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA support note tells you whether your browser has the View Transitions API; the first slide appears.
- 2Click the arrowsIn a supporting browser, the slide smoothly cross-fades and morphs into the next one — no hand-written animation.
- 3Click a dotJump directly to that slide, with the same automatic browser-driven transition.
- 4Try it in an unsupported browserThe carousel still works perfectly — it just swaps instantly instead of animating.
- 5Inspect the CSSNotice there's no transition or @keyframes rule driving the slide change at all — just one view-transition-name declaration.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
It marks an element as a named "subject" the browser should track across a startViewTransition call — if an element with that same name exists in both the before and after DOM snapshots, the browser automatically animates between its old and new state (position, size, opacity) rather than treating it as an unrelated element being removed and added.
The feature is checked with typeof document.startViewTransition === "function" before use — when it's missing, paint() is called directly with no wrapping transition call, so the carousel still functions correctly, just without the animated cross-fade/morph.
Yes — give the icon, the title, and the background each their own unique view-transition-name, and the browser will animate each one's before/after state independently rather than treating the whole slide as one blob.
Yes, in supporting browsers it also works for full page navigations (same-document or cross-document), which is a separate but related use of the same API — this snippet demonstrates the single-page, same-document version.
Arrows and dots are real labeled buttons, fully keyboard-operable via Left/Right arrow keys; consider also checking prefers-reduced-motion and skipping startViewTransition's animation (calling paint() directly) for users who've requested reduced motion.