Smooth transitions between UI states separate polished applications from mediocre ones. Historically, achieving those transitions meant reaching for JavaScript animation libraries — GSAP, Framer Motion, or hand-rolled requestAnimationFrame loops. The View Transitions API changes that equation by giving the browser native responsibility for capturing before/after snapshots and animating between them. And with Chrome 147’s introduction of element-scoped view transitions, the API just took a significant leap forward.
This post walks through how the View Transitions API works, where element-scoped transitions fit in, and how to start using them in production today — no animation library required.
How View Transitions Work
At its core, the View Transitions API follows a simple three-step process: the browser captures a snapshot of the current DOM state, you update the DOM, and then the browser captures a new snapshot and animates the transition between the two. The entire mechanism is powered by CSS pseudo-elements, which means you control the animations with plain CSS.
The entry point is document.startViewTransition(), which accepts a callback that performs the DOM update:
function handleNavigation(data) {
if (!document.startViewTransition) {
updateDOM(data);
return;
}
document.startViewTransition(() => {
updateDOM(data);
});
}
That’s it. The browser automatically cross-fades between the old and new states. No keyframes, no JavaScript animation logic, no libraries. The result is a smooth cross-fade that works as a progressive enhancement — browsers that don’t support the API simply get an instant DOM swap.
Anatomy of a Transition
When a view transition activates, the browser constructs a pseudo-element tree that looks like this:
::view-transition
└── ::view-transition-group(root)
└── ::view-transition-image-pair(root)
├── ::view-transition-old(root) /* screenshot of old state */
└── ::view-transition-new(root) /* live view of new state */
The ::view-transition-old(root) is a screenshot of the previous state, and ::view-transition-new(root) is a live representation of the new state. By default, the old view fades out while the new view fades in. Because these are standard CSS pseudo-elements, you can customize them with any CSS animation:
/* Override the default cross-fade with a slide transition */
::view-transition-old(root) {
animation: 300ms ease-out both slide-to-left;
}
::view-transition-new(root) {
animation: 300ms ease-out both slide-from-right;
}
@keyframes slide-to-left {
to { transform: translateX(-30px); opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); opacity: 0; }
}
Animating Individual Elements with view-transition-name
The real power emerges when you assign view-transition-name to specific elements. This tells the browser to track those elements independently, creating separate transition groups that animate with their own position, size, and opacity:
.hero-image {
view-transition-name: hero-image;
}
.page-title {
view-transition-name: page-title;
width: fit-content;
}
Now when a navigation occurs, the hero image smoothly morphs from its old position and size to the new one, while the page title does the same — each animating independently. The pseudo-element tree expands to include separate groups for each named element:
::view-transition
├── ::view-transition-group(root)
│ └── ...
├── ::view-transition-group(hero-image)
│ └── ::view-transition-image-pair(hero-image)
│ ├── ::view-transition-old(hero-image)
│ └── ::view-transition-new(hero-image)
└── ::view-transition-group(page-title)
└── ::view-transition-image-pair(page-title)
├── ::view-transition-old(page-title)
└── ::view-transition-new(page-title)
Each group can be styled independently. This is how you create those satisfying animations where a thumbnail on a listing page expands into a full-size image on the detail page — the two elements share the same view-transition-name for the duration of the transition, even though they’re completely different DOM nodes.
Element-Scoped Transitions: What Changed in Chrome 147
Until recently, document.startViewTransition() was the only way to trigger a view transition. It operated on the entire document, which carried two limitations: only one transition could run at a time, and the entire page freezes during the snapshot-and-animate cycle.
Chrome 147 (released March 2026) introduces element-scoped view transitions via Element.startViewTransition(). This method is available on arbitrary HTML elements and scopes the transition to a DOM subtree. The practical impact is significant:
- Concurrent transitions: Multiple elements can run their own transitions simultaneously. Two independent lists on the same page can each shuffle their items at the same time.
- Page stays interactive: The rest of the page outside the scoped element continues to respond to user input during the transition.
- Proper clipping: Transition pseudo-elements respect ancestor
overflow: hiddenand CSS transforms — no more content leaking outside containers. - Nested transitions: You can start an outer transition (e.g., swapping two panels) while inner transitions (e.g., reordering items within each panel) are still running.
Element-Scoped Transitions in Practice
Using element-scoped transitions is straightforward. Instead of calling document.startViewTransition(), call startViewTransition() on the specific element you want to scope to:
const grid = document.querySelector('.photo-grid');
function shuffleGrid() {
if (!grid.startViewTransition) {
rearrangeItems();
return;
}
grid.startViewTransition(() => {
rearrangeItems();
});
}
This scopes the transition to the .photo-grid element. Only items within that grid participate in the transition — the header, sidebar, and footer are unaffected and remain fully interactive.
For concurrent transitions on multiple independent containers, call startViewTransition() on each one:
function shuffleBothLists() {
const listA = document.querySelector('#list-a');
const listB = document.querySelector('#list-b');
// Both transitions run simultaneously
listA.startViewTransition(() => shuffle(listA));
listB.startViewTransition(() => shuffle(listB));
}
The CSS animation rules work exactly the same way — target the pseudo-elements with view-transition-name and customize animations as needed:
.grid-item {
view-transition-name: var(--item-id);
}
/* Apply bounce effect to all grid items */
.grid-item {
view-transition-class: grid-item;
}
::view-transition-group(.grid-item) {
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
animation-duration: 400ms;
}
Cross-Document Transitions for Multi-Page Apps
The View Transitions API isn’t limited to single-page applications. Cross-document view transitions work across full page navigations in multi-page apps. Since Chrome 126, you can enable them with a single CSS rule:
@view-transition {
navigation: auto;
}
This opts the page into automatic view transitions on same-origin navigations. When a user clicks a link to another page on your site, the browser captures the old page, loads the new page, and animates between them — all without JavaScript. Combined with view-transition-name on shared elements like headers and hero images, you get shared-element transitions across full page loads.
Practical Patterns
Filter Animations with Types
View transition types let you apply different animations depending on context. For example, a filter dropdown should animate differently when opening versus closing:
const transition = document.startViewTransition({
update: () => applyFilter(filterValue),
types: ['filter-change'],
});
html:active-view-transition-type(filter-change) {
&::view-transition-old(root) {
animation-name: fade-out, scale-down;
}
&::view-transition-new(root) {
animation-name: fade-in, scale-up;
}
}
Respecting Reduced Motion
Always respect the user’s motion preferences. A subtle animation is usually better than no animation at all:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}
Browser Support and Progressive Enhancement
Same-document view transitions are supported in Chrome 111+, Firefox 144+, and Safari 18+. Cross-document transitions work in Chrome 126+ and Safari 18.2+. Element-scoped transitions are currently Chrome 147+ (March 2026).
The API is designed as a progressive enhancement. The fallback pattern is simple: check for the method’s existence, and if it’s not available, update the DOM directly. This means you can start using view transitions today without worrying about breaking the experience for users on older browsers.
function updateWithTransition(updateFn) {
// Feature detection — instant update as fallback
if (!document.startViewTransition) {
updateFn();
return;
}
document.startViewTransition(() => updateFn());
}
The Bottom Line
The View Transitions API eliminates a category of JavaScript that never needed to exist — the boilerplate code for capturing element positions, calculating deltas, and manually animating between states. With element-scoped transitions now shipping in Chrome 147, the API handles concurrent animations, proper clipping, and page interactivity that were previously major pain points.
If your application has state transitions that should feel smooth — list reordering, filter changes, navigation, tab switching — the View Transitions API is worth adopting. Start with document.startViewTransition() for simple cross-fades, add view-transition-name for shared-element animations, and graduate to Element.startViewTransition() when you need concurrent or scoped transitions. The official documentation covers each pattern in detail with interactive demos.