Part 4 of 4 · CSS Shine Lab
The View Transition API
Animate between two views, a crossfade or a shared-element morph, by wrapping a DOM change so the browser tweens the old state into the new one.
- Innovation
Reach for the View Transition API when the change is bigger than a single element: swapping a whole panel, or moving between pages. Support is still uneven. Same-document transitions ship in Chrome and Safari, cross-document navigation ships in Chromium, and Firefox is still catching up. It degrades gracefully, though, so you can use it today and browsers that do not support it simply skip the animation.
Implementation
There are two ways in. In a single document, wrap a DOM change in
document.startViewTransition(() => { /* update the DOM */ }) and the browser
snapshots before and after and animates between them: a crossfade by default, or
a morph for any element you give a unique view-transition-name.
Across pages you do not need JavaScript at all. Opt in with
@view-transition { navigation: auto } and give matching view-transition-name
values to elements on both pages.
For single-page apps, resist hand-wiring startViewTransition into your router.
React is adding a dedicated <ViewTransition> component for exactly this, and it
is still experimental, so it is better to wait for it to stabilise than to build
something you will have to replace.
Crossfade a change
The simplest transition. Wrap a DOM update in startViewTransition and, with no
view-transition-name anywhere, the whole change is captured by the default
root group and old crossfades to new.
View source
HTML
<div class="vt-swap" id="vt-swap">
<span class="vt-swap__a">Panel A</span>
<span class="vt-swap__b">Panel B</span>
</div>
<button
type="button"
class="vt-swap-toggle"
onclick="
const swap = () => document.getElementById('vt-swap').classList.toggle('is-b');
document.startViewTransition ? document.startViewTransition(swap) : swap();
"
>
Swap panels
</button> CSS
.vt-swap {
display: grid;
place-items: center;
min-height: 6rem;
border-radius: var(--radius-md);
background: var(--color-accent);
color: var(--color-on-accent);
font-size: 1.25rem;
}
.vt-swap.is-b {
background: #7c3aed;
color: #fff;
}
/* Show one panel at a time; the button below toggles `is-b`. */
.vt-swap__b {
display: none;
}
.vt-swap.is-b .vt-swap__a {
display: none;
}
.vt-swap.is-b .vt-swap__b {
display: block;
}
.vt-swap-toggle {
margin-top: 0.75rem;
padding: 0.6rem 1.1rem;
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text);
font: inherit;
cursor: pointer;
}
/* No view-transition-name anywhere, so the change is captured by the default
`root` group and old simply crossfades to new. */ This is not just a demo. The theme switch in this site’s footer does exactly
this: toggling between light and dark wraps the change in
document.startViewTransition, so the whole page crossfades instead of flashing.
Where a browser does not support it yet, the switch just happens instantly. That is the point: it costs almost nothing to add, and the browsers that support it get the crossfade for free. Try it, bottom right.
Morph a named element
Give an element a unique view-transition-name and it becomes its own
transition group, so instead of crossfading it smoothly morphs between its old
and new size and position.
View source
HTML
<div class="vt-morph" id="vt-morph">
<div class="vt-morph__box"></div>
</div>
<button
type="button"
class="vt-morph-toggle"
onclick="
const move = () => document.getElementById('vt-morph').classList.toggle('is-end');
document.startViewTransition ? document.startViewTransition(move) : move();
"
>
Move and resize
</button> CSS
.vt-morph {
display: flex;
justify-content: flex-start;
}
.vt-morph.is-end {
justify-content: flex-end;
}
.vt-morph__box {
width: 5rem;
height: 5rem;
border-radius: var(--radius-md);
background: var(--color-accent);
/* A unique name makes the box its own transition group, so it smoothly morphs
between its old and new size/position instead of crossfading. */
view-transition-name: vt-morph-box;
}
.vt-morph.is-end .vt-morph__box {
width: 8rem;
height: 8rem;
background: #7c3aed;
}
.vt-morph-toggle {
margin-top: 0.75rem;
padding: 0.6rem 1.1rem;
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text);
font: inherit;
cursor: pointer;
}