View Transitions API
Browser-native morph between states and pages.
Imagine you click a thumbnail and watch it grow smoothly into a full hero image on the next page. For ten years that effect required a full-blown framework, hand-tuned JavaScript, FLIP libraries, and prayers. In 2023, browsers shipped a native API for it called the View Transitions API. One function call, document.startViewTransition(), and the browser does the whole cross-fade-and-morph for you. This is one of the most impactful new web features in a decade.
Two flavors: same-document and cross-document
There are two versions of the API:
- Same-document view transitions: animate between two states within one page. Click a tab, the content morphs. Available in Chromium-based browsers since 2023, Safari since 2024.
- Cross-document view transitions: animate between two pages, even traditional MPA navigations. Triggered by a CSS
@view-transitionrule. Newer (2024-2025), Chromium first. No JavaScript needed.
The basic same-document recipe
Same-document view transitions need three pieces: a function call that mutates the DOM, a view-transition-name on each element you want to animate, and (optionally) CSS to customize the animation.
function toggleView() {
// The default: cross-fade everything.
// Browsers without the API will just run the callback synchronously.
if (!document.startViewTransition) {
updateDOM();
return;
}
document.startViewTransition(() => updateDOM());
}
function updateDOM() {
document.querySelector('.app').classList.toggle('dark');
}That alone gives you a smooth cross-fade between the two visual states. Zero CSS, zero animation libraries. The browser takes a snapshot before your callback, runs the callback, takes a snapshot after, and animates between them.
Naming elements so they morph instead of fade
To make one specific element morph between the two states (rather than just fade), give it a unique view-transition-name on both sides. Same name = same element across the transition.
.thumbnail {
view-transition-name: hero-image;
}
/* After the transition, this is now the big hero. Same name. */
.hero {
view-transition-name: hero-image;
}Click a thumbnail, change .thumbnail to .hero, and the browser sees the same view-transition-name before and after, so it animates the size, position, and content across the transition. The morph is automatic.
view-transition-nameat any moment. If you put the same name on three list items, the browser doesn't know which is which and the transition fails.::view-transition-* pseudo-elements
During a transition the browser inserts a tree of pseudo-elements into the page that you can style:
::view-transition /* the root container */
::view-transition-group(name) /* the named group */
::view-transition-image-pair(name) /* old + new snapshots */
::view-transition-old(name) /* the before snapshot */
::view-transition-new(name) /* the after snapshot */These let you customize the easing, duration, or even use @keyframes to override the default cross-fade:
/* All transitions on the page: slower, custom easing */
::view-transition-group(*) {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* The hero image specifically: a slide-in instead of fade */
::view-transition-old(hero) { animation-name: slide-out; }
::view-transition-new(hero) { animation-name: slide-in; }
@keyframes slide-in {
from { transform: translateY(20px); opacity: 0; }
}
@keyframes slide-out {
to { transform: translateY(-20px); opacity: 0; }
}Cross-document view transitions
The dream: smooth transitions between two HTML pages, with no single-page-app framework, no JavaScript routing. Just a CSS rule and a normal anchor link. To opt in, add this to both pages:
@view-transition {
navigation: auto;
}With that rule on both source and destination pages, a normal <a href="..."> click triggers a cross- document view transition. Add matching view-transition-names on the shared elements and they morph across pages.
@supports (view-transition-name: auto) or a JS feature check, and design the experience to be fine without the animation.Try a same-document transition
Click the buttons. The element morphs between two layouts using view transitions. View the JS to see how thin the actual code is.
Practical tips
- Don't name everyelement. The cross-fade default handles the rest. Naming is for the "hero" pieces that must visibly morph.
- The transition callback should be fast and synchronous (or return a promise that resolves quickly). Long async work mid-transition looks bad.
- Honor
prefers-reduced-motion. Either skip the transition or shorten it. - For SPAs, integrate with your router. React Router v7, Next.js, and others have official APIs that wrap startViewTransition.
document.startViewTransitiondoesn't exist (old browser), call your DOM update directly. The site still works. That's how you can ship view transitions today without breaking anyone.Quiz
What's the minimum code to animate a same-document state change with View Transitions?
Recap
document.startViewTransition(cb)animates a same-document DOM change as a cross-fade.- Add
view-transition-nameto elements you want to morph across the change, not just fade. - Customize via
::view-transition-*pseudo-elements. @view-transition { navigation: auto; }enables cross-document transitions on normal link navigations.- Progressive enhancement is built in: missing API means the callback just runs. Ship it today.