The View Transitions API enables smooth, native-feeling transitions between DOM states or pages. Instead of jarring instant updates, users see animated changes that help them understand what changed. It's native browser capability—no JavaScript animation libraries needed.
How it works
When you trigger a view transition:
- Browser captures current visual state as a snapshot
- Your callback updates the DOM
- Browser captures new visual state
- Browser animates between old and new states
document.startViewTransition(() => {
// Update the DOM
document.getElementById('content').innerHTML = newContent;
});
The default animation is a cross-fade, but you can customize everything with CSS.
Browser support
| Browser | Support |
|---|---|
| Chrome 111+ | Full |
| Edge 111+ | Full |
| Opera 97+ | Full |
| Safari 18+ | Partial |
| Firefox | In development |
Always feature detect:
if (document.startViewTransition) {
document.startViewTransition(() => updateDOM());
} else {
updateDOM(); // Fallback: instant update
}
Basic usage
Simple cross-fade
function navigateTo(href) {
if (!document.startViewTransition) {
window.location.href = href;
return;
}
document.startViewTransition(async () => {
const response = await fetch(href);
const html = await response.text();
document.body.innerHTML = new DOMParser()
.parseFromString(html, 'text/html')
.body.innerHTML;
});
}
SPA route changes
// React example
function navigate(path) {
if (!document.startViewTransition) {
setRoute(path);
return;
}
document.startViewTransition(() => {
setRoute(path);
});
}
Customizing with CSS
Default pseudo-elements
The API creates pseudo-elements you can style:
/* The old content (fading out) */
::view-transition-old(root) {
animation: fade-out 0.3s ease-out;
}
/* The new content (fading in) */
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
Slide animation
::view-transition-old(root) {
animation: slide-out 0.3s ease-out;
}
::view-transition-new(root) {
animation: slide-in 0.3s ease-in;
}
@keyframes slide-out {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
@keyframes slide-in {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
Named transitions
Give elements unique names for targeted animations:
.hero-image {
view-transition-name: hero;
}
.page-title {
view-transition-name: title;
}
Now animate them specifically:
::view-transition-old(hero) {
animation: scale-down 0.4s ease-out;
}
::view-transition-new(hero) {
animation: scale-up 0.4s ease-in;
}
::view-transition-old(title) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(title) {
animation: fade-in 0.2s ease-in 0.1s both;
}
Shared element transitions
When the same element exists on both pages:
/* Product card on list page */
.product-card {
view-transition-name: product-123;
}
/* Same product on detail page */
.product-hero {
view-transition-name: product-123;
}
The browser automatically animates the element from its old position to its new position.
JavaScript API details
The transition object
const transition = document.startViewTransition(() => {
updateDOM();
});
// Wait for transition to complete
await transition.finished;
// Wait for new content to be captured
await transition.updateCallbackDone;
// Skip the animation
transition.skipTransition();
Transition lifecycle
const transition = document.startViewTransition(async () => {
await fetchAndUpdate();
});
// Ready: old snapshot captured
await transition.ready;
console.log('Old state captured');
// updateCallbackDone: DOM updated
await transition.updateCallbackDone;
console.log('DOM updated');
// finished: animation complete
await transition.finished;
console.log('Transition complete');
Framework integration
React with Next.js
'use client';
import { useRouter } from 'next/navigation';
export function Link({ href, children }) {
const router = useRouter();
function handleClick(e: React.MouseEvent) {
e.preventDefault();
if (!document.startViewTransition) {
router.push(href);
return;
}
document.startViewTransition(() => {
router.push(href);
});
}
return (
<a href={href} onClick={handleClick}>
{children}
</a>
);
}
Vue
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function navigate(path) {
if (!document.startViewTransition) {
router.push(path);
return;
}
document.startViewTransition(() => {
router.push(path);
});
}
</script>
Direction-aware transitions
Apply different animations based on navigation direction:
let direction = 'forward';
function navigate(href, dir = 'forward') {
direction = dir;
document.documentElement.dataset.direction = dir;
document.startViewTransition(() => {
setRoute(href);
});
}
[data-direction="forward"]::view-transition-old(root) {
animation: slide-left-out 0.3s ease-out;
}
[data-direction="forward"]::view-transition-new(root) {
animation: slide-left-in 0.3s ease-in;
}
[data-direction="back"]::view-transition-old(root) {
animation: slide-right-out 0.3s ease-out;
}
[data-direction="back"]::view-transition-new(root) {
animation: slide-right-in 0.3s ease-in;
}
Performance considerations
| Practice | Reason |
|---|---|
| Keep callbacks fast | Long DOM updates delay animation start |
| Use CSS animations | GPU accelerated, smooth |
| Limit named elements | Each name creates capture work |
| Avoid layout thrashing | Causes jank during capture |
Accessibility
Respect reduced motion preferences:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Common patterns
Page header morphing
.page-header {
view-transition-name: header;
}
::view-transition-old(header),
::view-transition-new(header) {
animation: none;
mix-blend-mode: normal;
}
List to detail
// On list page
function openDetail(id) {
document.querySelector(`#item-${id}`).style.viewTransitionName = 'item';
document.startViewTransition(() => {
setRoute(`/items/${id}`);
});
}
.detail-hero {
view-transition-name: item;
}
The clicked item animates to become the detail hero.
Summary
The View Transitions API enables smooth animated transitions between DOM states with minimal code. Call document.startViewTransition() with a callback that updates the DOM. Customize animations with CSS pseudo-elements. Use named transitions for shared element animations. Feature detect and provide fallbacks for unsupported browsers. Respect reduced motion preferences for accessibility.
