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. Chrome 111 (2023), Safari 18.0 (2024), Firefox 144 (October 2025). That last one is what made it Baseline.
- Cross-document view transitions: animate between two pages, even traditional MPA navigations. Triggered by a CSS
@view-transitionrule, no JavaScript needed. Chrome 126 and Safari 18.2. Not in Firefox, and not currently on a roadmap, so treat it as a progressive enhancement.
view-transition-name, and only one is portable. match-element is in Chrome 137, Safari 18.4, and Firefox 144. auto is Safari-only and has never shipped in Chromium. They also mean different things: auto derives the name from the element's id, which is what lets it match across documents, while match-element matches on element identity and is therefore same-document only.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-name at 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.
@view-transition ignores the rule and navigates normally, which is the correct fallback. If you do need to branch in JavaScript, test for window.CSSViewTransitionRule. What you should not do is gate on @supports (view-transition-name: auto): that value is Safari-only, so Chrome would fail a check for a feature it has.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 every element. 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.startViewTransition doesn'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.