The View Transitions API turns a hard DOM swap into an animated transition
with one function call: document.startViewTransition(() => { /* your DOM update */ }).
The browser snapshots the before and after states and tweens between them.
Below is a tiny travel journal. Click a destination: the photo and the title morph
into the detail view. Flip the transitions toggle to feel what the API adds, or try the
slide and zoom presets.
Support check (live)
Same-document transitions are Baseline: Chrome/Edge 111+, Safari 18+, Firefox 138+.
Cross-document transitions (@view-transition { navigation: auto; }) are newer:
Chromium 126+, Safari 18.2+, Firefox still trailing. This demo uses the same-document API,
and it skips transitions entirely when you prefer reduced motion.
The demo
data-vt-preset values
The demo picks its panel animation by setting a data-vt-preset attribute on
the <html> element just before each transition. The CSS keys each
animation off that attribute, so only the matching
::view-transition-old/new rules apply. Valid values:
(absent)— default crossfade, with the photo and title morphing as shared elements (Auto preset).slide-fwd— panel slides right-to-left (forward navigation).slide-back— panel slides left-to-right (back navigation).zoom— panel scales and fades.
Live: <html> currently has (absent)
Why not just CSS?
CSS can animate an element. It cannot animate a change. The moment you swap content, the old DOM is gone, so there is nothing left to animate out. Doing this in pure CSS means keeping two copies of the content alive at once, animating between them, then cleaning up. That is the FLIP pattern, and the measuring and cleanup is where the bugs live.
This API snapshots the page before your DOM update and after it, then hands you both
snapshots as pseudo-elements you can animate. No duplicate live DOM. No measuring
rectangles. The shared-element morph is one view-transition-name on both ends
and the browser does the rest. The slide preset above is about ten lines of CSS.
And the fallback is free. No support means the update just happens instantly. It is polish, not a dependency.
The code
1. The whole basic API
document.startViewTransition(() => {
// any DOM update: swap views, re-render a list, toggle a class
showDetail(place);
});
2. Shared-element morph (photo and title fly into place)
// give both ends the SAME name; each name must be unique per snapshot
thumbImg.style.viewTransitionName = 'wander-img-amalfi';
titleEl.style.viewTransitionName = 'wander-title-amalfi';
// ...render the detail view with the same two names...
const t = document.startViewTransition(() => showDetail('amalfi'));
t.finished.finally(() => {
thumbImg.style.viewTransitionName = '';
titleEl.style.viewTransitionName = '';
});
3. Custom animation, pure CSS
::view-transition-old(wander) {
animation: slide-out 380ms ease-in both;
}
::view-transition-new(wander) {
animation: slide-in 380ms ease-out both;
}
@keyframes slide-out { to { transform: translateX(-72px); opacity: 0; } }
@keyframes slide-in { from { transform: translateX(72px); } }
4. Feature-detect and respect reduced motion
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
function update(fn) {
if (document.startViewTransition && !reduce) {
document.startViewTransition(fn);
} else {
fn(); // plain instant swap: the free fallback
}
}
Important notes
- The fallback is doing nothing. A browser without the API runs your update function directly and the user sees an instant cut. No polyfill needed; you are adding polish, not a dependency. Flip the toggle above to feel the no-transition version.
- Names must be unique. Two visible elements sharing one
view-transition-namein the same snapshot abort the transition. This demo generates one name per photo and per title (wander-img-amalfi,wander-title-amalfi, …). - Morphs and panel motion don't mix. On the slide and zoom presets the whole journal panel is the animated element, so the shared-element morph is skipped there. It only runs on Auto, where the panel itself just crossfades.
- Same-document vs cross-document. This page demos same-document
transitions. For classic multi-page sites,
@view-transition { navigation: auto; }gives you crossfades between full page loads with zero JavaScript, where supported. - Reduced motion. View transitions are motion. If the user asked for less
of it, skip the transition. This demo checks
prefers-reduced-motionlive and swaps instantly when it is set to reduce. - Real use, on my own site. The theme toggle on
dominic.balass.one wraps its
data-themeswap indocument.startViewTransition(), so the whole page crossfades between themes instead of snapping. Four lines, feature-detected, skipped for reduced motion.