View Transitions crossfade
The browser snapshots the old and new view and crossfades them for you with document.startViewTransition().
CSSJavaScriptIntermediate
<div class="box a" id="box"><h2 id="t">Card A</h2><button id="b">Morph</button></div>
.box{width:min(300px,84vw);padding:30px;border-radius:18px;text-align:center}
.box.a{background:#f08a5d;color:#270100}
.box.b{background:#f3d9c8;color:#270100;width:min(360px,90vw);padding:50px 30px;border-radius:30px}
h2{margin:0 0 14px}
button{font:inherit;font-weight:700;background:#270100;color:#fff;border:0;border-radius:10px;padding:10px 18px;cursor:pointer}
.box{view-transition-name:card} /* same name = the browser morphs one into the other */
::view-transition-group(card){animation-duration:.5s;animation-timing-function:cubic-bezier(.2,.8,.2,1)}
const box = document.getElementById('box');
const t = document.getElementById('t');
function swap() {
const toB = box.classList.contains('a');
box.classList.toggle('a', !toB);
box.classList.toggle('b', toB);
t.textContent = toB ? 'Card B' : 'Card A';
}
document.getElementById('b').onclick = () => {
if (document.startViewTransition) document.startViewTransition(swap);
else swap(); // fallback: no animation
};
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>View Transitions</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
.box{width:min(300px,84vw);padding:30px;border-radius:18px;text-align:center}
.box.a{background:#f08a5d;color:#270100}
.box.b{background:#f3d9c8;color:#270100;width:min(360px,90vw);padding:50px 30px;border-radius:30px}
h2{margin:0 0 14px}
button{font:inherit;font-weight:700;background:#270100;color:#fff;border:0;border-radius:10px;padding:10px 18px;cursor:pointer}
.box{view-transition-name:card} /* same name = the browser morphs one into the other */
::view-transition-group(card){animation-duration:.5s;animation-timing-function:cubic-bezier(.2,.8,.2,1)}
</style></head>
<body>
<div class="box a" id="box"><h2 id="t">Card A</h2><button id="b">Morph</button></div>
<script>
const box = document.getElementById('box');
const t = document.getElementById('t');
function swap() {
const toB = box.classList.contains('a');
box.classList.toggle('a', !toB);
box.classList.toggle('b', toB);
t.textContent = toB ? 'Card B' : 'Card A';
}
document.getElementById('b').onclick = () => {
if (document.startViewTransition) document.startViewTransition(swap);
else swap(); // fallback: no animation
};
</script>
</body></html>
Good for
Smooth view or page changes with almost no code. Also works across real page loads with @view-transition { navigation: auto }.
Watch out for
Supported in Chrome, Edge and Safari 18+. Always fall back to a plain update when the API is missing.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.