Smooth page and UI transitions, built into the browser, with little to no JavaScript. This tutorial covers exactly how the View Transitions API works, the difference between same-document and cross-document transitions, real browser support in 2026, and the accessibility details most guides skip.
⚡ Quick Answer
The CSS View Transitions API lets a browser automatically animate the change between two UI states — either inside one page (same-document) or between two separate pages (cross-document) — by snapshotting the old view and the new view, then cross-fading or morphing between them. Same-document transitions need one line of JavaScript: document.startViewTransition(). Cross-document transitions need zero JavaScript — just a @view-transition CSS rule on both pages.
📋 Table of Contents
- What Is the View Transitions API?
- How It Works Under the Hood
- Same-Document vs Cross-Document — What’s the Difference?
- Same-Document Transitions (Single-Page Apps)
- Cross-Document Transitions (Multi-Page Sites)
- Animating Specific Elements with view-transition-name
- Customizing the Animation
- Browser Support in 2026
- Accessibility: Reduced Motion, Focus & Scroll
- Common Pitfalls
- FAQ
What Is the View Transitions API?
For as long as the web has existed, navigating between two pages has meant one thing: the old page disappears, there’s a blank flash, and the new page appears. Single-page apps solved this with heavy JavaScript routers — but that traded one problem for another: slower initial loads, larger bundles, and a lot of custom animation code to maintain.
The View Transitions API removes that trade-off. The browser itself takes a “screenshot” of the current view, swaps in the new content, and animates between the two — without you writing manual position-tracking or animation-cleanup logic.
How It Works Under the Hood
When a view transition starts, the browser builds a temporary tree of pseudo-elements that sit in an overlay above your actual page content. Understanding this tree is the key to understanding everything else in this tutorial — including how to customize the animation later.
::view-transition // the overlay root, covers the whole viewport
::view-transition-group(name) // one per named element (or "root")
::view-transition-image-pair(name) // holds both snapshots
::view-transition-old(name) // static snapshot of the OLD view
::view-transition-new(name) // live snapshot of the NEW view
By default, the browser cross-fades ::view-transition-old out and ::view-transition-new in. You can override any of these pseudo-elements with your own CSS animations — covered in the customization section below.
Same-Document vs Cross-Document — What’s the Difference?
This is the single most important distinction in the whole API, and it determines whether you need JavaScript at all.
| Same-Document | Cross-Document |
|---|---|
| Use case: Single-page app DOM update | Use case: Navigating between two separate HTML pages |
Trigger: JavaScript — document.startViewTransition() | Trigger: CSS only — @view-transition at-rule |
| JavaScript required? Yes | JavaScript required? No |
| Scope: Works anywhere, any origin | Scope: Same-origin navigations only |
| Baseline support: Chrome 111+, Safari 18+, Firefox 144+ | Support: Chrome 126+, Safari 18.2+, Firefox in progress |
Same-Document Transitions (Single-Page Apps)
Use this pattern any time you’re updating the DOM with JavaScript — filtering a list, swapping a detail view, toggling a theme — and you want the change to animate instead of snapping instantly.
// Wrap your DOM update inside startViewTransition()
function updateView(newContent) {
// Feature-check: not all browsers support this yet
if (!document.startViewTransition) {
renderContent(newContent); // fallback: just update, no animation
return;
}
// The callback runs your actual DOM mutation.
// The browser snapshots before AND after this callback runs.
document.startViewTransition(() => {
renderContent(newContent);
});
}
ViewTransition object with promises you can hook into: .ready resolves when the animation is about to start, .updateCallbackDone resolves once your DOM-mutation callback finishes, and .finished resolves when the whole transition (including animation) is complete. Most simple use cases never need these — but they matter for chaining transitions or cleaning up state.Live Demo — Click a Tile
Click any tile — the swap is animated by document.startViewTransition(), not a hand-written CSS animation.
Cross-Document Transitions (Multi-Page Sites)
This is the feature most developers are excited about: native page-to-page transitions on a traditional multi-page website, with zero JavaScript and zero client-side router.
/* Add this exact rule to the stylesheet of BOTH pages —
the one being navigated FROM and the one being navigated TO. */
@view-transition {
navigation: auto;
}
https://example.com to https://blog.example.com (a different subdomain) will not animate, even with the rule in place on both.That single rule gives you a default cross-fade. If you click a link between two pages with this rule active, in a supporting browser, the navigation fades smoothly instead of snapping.
<meta name="view-transition" content="same-origin">. The spec moved to the @view-transition CSS at-rule because a meta tag couldn’t be conditionally scoped with media queries or cascade layers. If you see the meta tag in an older tutorial, it’s outdated — use the CSS at-rule instead.Animating Specific Elements with view-transition-name
The default cross-fade animates the entire viewport as one block. To make a specific element — like a product thumbnail morphing into a full-size hero image — animate independently and smoothly between two states, give it a unique view-transition-name.
/* On the LIST page: the thumbnail */
.product-thumb {
view-transition-name: product-hero;
}
/* On the DETAIL page: the full-size image — SAME name */
.product-hero-image {
view-transition-name: product-hero;
}
/* The browser automatically morphs size and position
between the two elements — no manual FLIP animation needed. */
view-transition-name at the same time, the transition silently fails for both. For a list of repeating items (a card grid), generate the name dynamically — e.g. view-transition-name: product-${id} via inline style or CSS custom property — rather than hardcoding one static name.// In your render logic, set a unique name per item via a CSS variable
cardElement.style.setProperty('--vt-name', `product-${product.id}`);
.product-card {
view-transition-name: var(--vt-name);
}
Customizing the Animation
The default cross-fade is fine for a quick win, but real interfaces usually want something more deliberate — a slide, a scale, a directional motion based on whether the user navigated forward or back.
/* Old view slides out to the left */
::view-transition-old(root) {
animation: slide-out-left 0.3s ease-in forwards;
}
/* New view slides in from the right */
::view-transition-new(root) {
animation: slide-in-right 0.3s ease-out forwards;
}
@keyframes slide-out-left {
to { transform: translateX(-30px); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(30px); opacity: 0; }
}
For direction-aware transitions (e.g. “next step” slides left, “back” slides right), use view transition types — a more recent addition to the spec that lets you tag a transition and branch your CSS on it.
document.startViewTransition({
update: () => renderNextStep(),
types: ['forward'] // or ['backward']
});
:active-view-transition-type(forward) ::view-transition-new(root) {
animation: slide-in-right 0.3s ease-out;
}
:active-view-transition-type(backward) ::view-transition-new(root) {
animation: slide-in-left 0.3s ease-out;
}
Browser Support in 2026
Same-document transitions are now Baseline — meaning they work consistently across all major engines. Cross-document support is newer and narrower. Here’s the current state:
| Browser | Same-document | Cross-document |
|---|---|---|
| Chrome / Edge | ✓ 111+ | ✓ 126+ |
| Safari | ✓ 18+ | ✓ 18.2+ |
| Firefox | ✓ 144+ | In progress |
Accessibility: Reduced Motion, Focus & Scroll
This is the section most tutorials skip, and it’s the one that determines whether your transitions are a delightful detail or an accessibility regression.
Respect prefers-reduced-motion
The browser does not automatically disable view transitions for users who’ve set their OS to reduce motion. You have to do it yourself.
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Focus and scroll position
For same-document transitions, the new content can sometimes render without inheriting the correct focus or scroll position, since the DOM update happens inside your callback. Always explicitly set focus on the most relevant new element after the transition’s updateCallbackDone promise resolves, rather than assuming the browser handles it for you.
const transition = document.startViewTransition(() => {
renderDetailView(item);
});
// Move focus to the new heading once the DOM swap is done
transition.updateCallbackDone.then(() => {
document.querySelector('h1')?.focus();
});
Common Pitfalls
- Forgetting the rule on one of the two pages. Cross-document transitions need
@view-transitionon both the origin and the destination — adding it to only one page silently does nothing. - Duplicate view-transition-name values. Any two elements sharing a name at the same moment will both fail to transition, with no console error to warn you.
- Animating layout-shifting properties. Avoid animating properties like
widthortopdirectly inside view transition pseudo-elements — stick totransformandopacityfor smooth, jank-free results. - Assuming it works cross-origin. Cross-document transitions are same-origin only as of 2026 — a subdomain change or different port breaks it.
- Skipping the reduced-motion override. This is the most commonly missed accessibility step — test it explicitly, don’t assume the browser handles it.
Frequently Asked Questions
What is the CSS View Transitions API?
The View Transitions API is a browser feature that automatically animates the change between two DOM states or two documents. It works by taking a snapshot of the old view, capturing the new view, and cross-fading or morphing between them using CSS — without manual animation logic in JavaScript.
What is the difference between same-document and cross-document view transitions?
Same-document view transitions animate UI changes within a single page, such as a single-page app updating its DOM, and are triggered with document.startViewTransition(). Cross-document view transitions animate navigation between two separate HTML pages on a multi-page site, enabled purely with the @view-transition CSS at-rule and no JavaScript.
Do I need JavaScript for CSS view transitions?
No, not for cross-document transitions between separate pages — those only need the @view-transition CSS at-rule on both the origin and destination pages. JavaScript is only required for same-document view transitions inside a single-page app, where you call document.startViewTransition() to wrap your DOM update.
Which browsers support the View Transitions API?
Same-document view transitions are Baseline, supported in Chrome 111+, Safari 18+, and Firefox 144+. Cross-document view transitions have narrower support: Chrome 126+ and Safari 18.2+, while Firefox cross-document support is still in progress as of 2026. The feature degrades gracefully in unsupported browsers — the animation is simply skipped.
How do you animate a specific element during a view transition?
Assign a unique view-transition-name to the element in CSS. The browser then animates that element separately from the rest of the page as a shared-element transition instead of a simple cross-fade. Each name must be unique on the page at any one time, or the transition silently fails for both elements sharing it.
Are view transitions accessible?
Not automatically. The browser does not disable view transitions for users with prefers-reduced-motion enabled — developers must add that override themselves using an @media (prefers-reduced-motion: reduce) block targeting the view transition pseudo-elements. Developers should also explicitly restore focus and scroll position after same-document transitions complete.
Summary
The View Transitions API closes a gap that’s existed on the web for over two decades: smooth, native transitions between views — without the JavaScript weight of a full client-side router.
- Single-page app? Wrap your DOM update in
document.startViewTransition(). - Multi-page site? Add
@view-transition { navigation: auto; }to both pages — zero JavaScript. - Want a specific element to morph smoothly? Give it a matching
view-transition-nameon both states. - Always add a
prefers-reduced-motionoverride and verify focus/scroll behaviour — the browser won’t do this for you. - Ship it today — the feature degrades gracefully, so there’s no risk to using it as a progressive enhancement even while Firefox finishes cross-document support.
