View Transitions
When content on a page changes, for example a new tab opens or an item moves from one list to another, it normally just snaps into its new state. Native phone apps instead slide, fade and morph things into place, which helps people follow what changed. View transitions bring that to the web with very little code.
In this lesson you will start a view transition from JavaScript, customise its animation with CSS, make individual elements glide to their new position with view-transition-name, and set up transitions between separate pages.
How a view transition works
A view transition has three steps, and the browser handles the hard parts:
- The browser takes a snapshot (a picture) of the current page.
- Your code updates the page: changes text, adds or moves elements, anything.
- The browser takes a snapshot of the new state and animates from the old picture to the new one.
By default the animation is a smooth crossfade of the whole page. Because the browser works with snapshots, your update code does not need to know anything about animation.
Your first view transition
For changes within one page, you call document.startViewTransition() and pass it a function that updates the page. Not every browser in use supports it, so always check first and fall back to a plain update:
function showArrivals() {
// ...change the page here...
}
if (document.startViewTransition) {
document.startViewTransition(showArrivals);
} else {
showArrivals(); // older browsers: same change, no animation
}Here is a working example. Each click swaps the content of the board.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>First view transition</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; }
.board {
max-width: 360px;
padding: 16px;
border-radius: 12px;
background: #1e293b;
color: #fde68a;
}
.board h2 { margin-top: 0; }
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 400ms;
}
</style>
</head>
<body>
<button id="switch" type="button">Show arrivals</button>
<div class="board">
<h2 id="title">Departures, Lucknow Jn</h2>
<p id="first">12:05 Shatabdi to Delhi</p>
<p id="second">13:40 Express to Varanasi</p>
</div>
<script>
const button = document.querySelector('#switch');
const title = document.querySelector('#title');
const first = document.querySelector('#first');
const second = document.querySelector('#second');
let showingDepartures = true;
function swapBoard() {
showingDepartures = !showingDepartures;
title.textContent = showingDepartures ? 'Departures, Lucknow Jn' : 'Arrivals, Lucknow Jn';
first.textContent = showingDepartures ? '12:05 Shatabdi to Delhi' : '11:50 Mail from Kolkata';
second.textContent = showingDepartures ? '13:40 Express to Varanasi' : '14:15 Express from Jaipur';
button.textContent = showingDepartures ? 'Show arrivals' : 'Show departures';
}
button.addEventListener('click', () => {
if (!document.startViewTransition) {
swapBoard();
return;
}
document.startViewTransition(swapBoard);
});
</script>
</body>
</html>The old board fades out while the new one fades in. The ::view-transition-old(root) and ::view-transition-new(root) rules slow the default crossfade down to 400ms.
Customising the animation with CSS
During a transition the browser builds a temporary tree of pseudo-elements on top of the page, which you can style like any other element:
| Pseudo-element | What it is |
|---|---|
::view-transition | The layer that holds everything during the transition |
::view-transition-group(name) | Moves and resizes one snapshot from its old place to its new place |
::view-transition-image-pair(name) | Holds the old and new pictures together |
::view-transition-old(name) | The picture of the old state, fading out |
::view-transition-new(name) | The picture of the new state, fading in |
The whole page has the name root. Since these are normal pseudo-elements, you can give them your own keyframe animations. This makes the old page slide out to the left and the new one slide in from the right:
@keyframes slide-out { to { translate: -100% 0; } }
@keyframes slide-in { from { translate: 100% 0; } }
::view-transition-old(root) {
animation: slide-out 300ms ease-in both;
}
::view-transition-new(root) {
animation: slide-in 300ms ease-out both;
}Moving single elements with view-transition-name
The real magic appears when you give an element its own view-transition-name. The browser then snapshots that element separately and moves it smoothly from its old position and size to its new one, even if it moved to a completely different part of the page.
Each name must be unique on the page at the moment the snapshot is taken. If two visible elements share a name, the browser skips the animation and just applies the change.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Moving elements</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; }
.lists { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 480px; }
.lists section { padding: 12px; border-radius: 10px; background: #f1f5f9; min-height: 220px; }
.lists h2 { margin: 0 0 8px; font-size: 1rem; }
ul { list-style: none; margin: 0; padding: 0; }
li { margin-bottom: 6px; }
li button {
width: 100%;
padding: 8px;
border: 0;
border-radius: 6px;
background: #2563eb;
color: white;
font: inherit;
text-align: start;
cursor: pointer;
}
#team li button { background: #15803d; }
::view-transition-group(*) { animation-duration: 350ms; }
</style>
</head>
<body>
<p>Click a player to move them between the lists.</p>
<div class="lists">
<section><h2>Squad</h2>
<ul id="squad">
<li style="view-transition-name: p-aarav"><button type="button">Aarav</button></li>
<li style="view-transition-name: p-priya"><button type="button">Priya</button></li>
<li style="view-transition-name: p-rohan"><button type="button">Rohan</button></li>
<li style="view-transition-name: p-meera"><button type="button">Meera</button></li>
</ul>
</section>
<section><h2>Playing XI</h2>
<ul id="team"></ul>
</section>
</div>
<script>
const squad = document.querySelector('#squad');
const team = document.querySelector('#team');
document.querySelector('.lists').addEventListener('click', (event) => {
const item = event.target.closest('li');
if (!item) return;
const move = () => {
(item.parentElement === squad ? team : squad).append(item);
};
if (!document.startViewTransition) {
move();
return;
}
document.startViewTransition(move);
});
</script>
</body>
</html>Each player has a unique name such as p-aarav, so each one glides to its new list. The other players also slide up to fill the gap, because they have names too and their positions changed. The * in ::view-transition-group(*) targets every named group at once.
Transitions between pages
So far everything happened inside one page. View transitions can also animate the jump from one page of your site to another, with no JavaScript at all. Add this rule to the CSS of both pages:
@view-transition {
navigation: auto;
}
/* Optional: elements with the same name on both pages morph */
.site-logo {
view-transition-name: site-logo;
}Now, when a visitor follows a link to another page on the same site, the browser crossfades from the old page to the new one. Elements with the same view-transition-name on both pages, such as a logo or a product photo that appears on a list page and a detail page, move smoothly from one position to the other.
Respect reduced motion
Large sliding or morphing animations can make some people feel unwell. Turn them off for users who have asked their device for reduced motion:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}The page still changes; it just changes instantly.
Common mistakes
- No feature check. Calling
document.startViewTransition()in a browser that does not have it throws an error and the update never happens. Always check and fall back. - Duplicate names. Two visible elements with the same
view-transition-namecancel the animation. Give every element its own name. - Long animations. View transitions should feel quick. Keep them around 200ms to 400ms so people are never kept waiting.
- Forgetting both pages. For transitions between pages, the
@view-transitionrule must be in the CSS of the old and the new page.
Key points
- A view transition snapshots the old state, runs your update, snapshots the new state and animates between them.
- Use
document.startViewTransition(updateFunction)within a page, with a fallback when it is missing. - Style the animation through pseudo-elements such as
::view-transition-old(root)and::view-transition-new(root). view-transition-namegives an element its own snapshot so it moves smoothly to its new place; names must be unique.@view-transition { navigation: auto; }on both pages animates same-origin page changes in supporting browsers.- Turn animations off under
prefers-reduced-motion: reduce.
Exercise
Build a photo viewer with a small thumbnail and a "Make large" button. Clicking the button toggles a class that makes the image much bigger and moves it below the button. Give the image a view-transition-name and use document.startViewTransition() (with a fallback) so that the image smoothly grows and moves instead of jumping.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Photo viewer</title>
<style>
.viewer { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.photo { width: 100px; view-transition-name: main-photo; }
.viewer.large .photo { width: 320px; order: 1; }
</style>
</head>
<body>
<div class="viewer">
<img class="photo" src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" alt="Illustration of hills and a river at sunrise">
<button type="button" id="size">Make large</button>
</div>
<script>
const viewer = document.querySelector('.viewer');
const button = document.querySelector('#size');
function toggleSize() {
viewer.classList.toggle('large');
button.textContent = viewer.classList.contains('large') ? 'Make small' : 'Make large';
}
button.addEventListener('click', () => {
if (!document.startViewTransition) {
toggleSize();
return;
}
document.startViewTransition(toggleSize);
});
</script>
</body>
</html>Because the image has its own view-transition-name, the browser animates its size and position from the old layout to the new one. The fallback makes the same change without animation.
Quick Quiz
1. What does document.startViewTransition(update) do?
The browser captures the before and after states around your update function and animates between them.
2. Two visible elements both have view-transition-name: card. What happens?
Names must be unique at the time of the snapshot; duplicates make the browser skip the transition.
3. Which pseudo-element represents the picture of the new page state?
::view-transition-new() shows the new state, while ::view-transition-old() shows the old one.
4. What is needed for a transition between two pages of the same site?
Cross-document transitions are switched on with CSS on both the old and the new page.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
