Welcome back, future web animation wizard! In our previous chapters, you’ve mastered the fundamentals of the View Transition API, creating smooth, engaging transitions between pages or across significant document-level changes. You’ve seen how document.startViewTransition() can transform your user’s experience.
But what if you don’t need to transition the entire document? What if you want to animate changes within a specific part of your page, like reordering items in a list, toggling the visibility of a component, or dynamically rearranging dashboard widgets? This is where the exciting new world of Scoped View Transitions comes in!
In this chapter, we’re diving deep into Scoped View Transitions, a powerful extension that lets you apply the magic of view transitions to any DOM subtree. We’ll build a mini-dashboard where you can dynamically reorder and toggle widgets, making the changes feel fluid and professional. Get ready to elevate your UI game!
To make the most of this chapter, you should be comfortable with:
- Basic HTML, CSS, and JavaScript.
- The core concepts of the View Transition API, including
document.startViewTransition()and theview-transition-nameCSS property, as covered in previous chapters.
Core Concepts: Unleashing Element-Scoped Transitions
The original View Transition API (which uses document.startViewTransition()) is fantastic for full-page navigations or major layout shifts affecting the entire document. However, it has a limitation: only one transition can run at a time, and it always operates on the entire document’s snapshot.
Scoped View Transitions introduce a game-changing capability: the ability to initiate a view transition on a specific DOM element and its children. This means you can have multiple, independent transitions happening simultaneously on different parts of your page! Imagine the possibilities for complex, interactive UIs!
The Star of the Show: element.startViewTransition()
Forget document.startViewTransition() for a moment (just for this specific use case, of course!). With Scoped View Transitions, we get a new method: element.startViewTransition().
Let’s break down what this means:
- Targeted Transitions: Instead of
document, you callstartViewTransition()directly on the parent element of the subtree you want to transition. For example, if you have adivcontaining several widgets, and you want to animate changes within thatdiv, you’d callmyDashboardDiv.startViewTransition(). - Independent Animation: Each
element.startViewTransition()call creates its own independent transition. This is how you achieve concurrent animations! One widget could be fading out while another is sliding into place, all without interfering with each other. view-transition-nameStill Reigns: Just like with document-scoped transitions,view-transition-nameis crucial. It tells the browser which elements are “the same” before and after the DOM change, allowing the browser to create a smooth animation between their old and new states. The key difference is that these names are now scoped to the element on whichstartViewTransition()was called.
Why is this a big deal? Think about a dashboard where users can drag and drop widgets, resize them, or add/remove new ones. With document-scoped transitions, animating each of these changes independently and concurrently would be incredibly difficult, if not impossible, without complex JavaScript animation libraries. Scoped View Transitions make this fluid, native-like experience much more accessible.
Current Status (as of 2025-12-05)
It’s important to note that Scoped View Transitions are an experimental feature and an extension to the main View Transition API. While the core View Transition API (document-scoped) is widely supported in modern browsers, scoped transitions are still under active development and testing by the WICG (Web Incubator Community Group).
- Browser Support: As of late 2025, you’ll likely need to enable an experimental flag in browsers like Chrome (e.g., navigate to
chrome://flags/#view-transitions-on-elementand enable it) to useelement.startViewTransition(). Firefox and Safari are actively working on their implementations. - Official Spec: You can follow the latest discussions and specification progress on the WICG’s GitHub repository: WICG/shared-element-transitions/blob/main/scoped-transitions.md
This means that while we’re learning it today, it’s something to use with progressive enhancement in mind for production, ensuring a graceful fallback for browsers that don’t yet support it. But for learning and experimenting, it’s perfect!
Step-by-Step Implementation: Building Our Dynamic Dashboard
Let’s get our hands dirty! We’ll create a simple dashboard with a few “widget” cards. We’ll then implement functionality to reorder two widgets and toggle the visibility of another, all with beautiful, scoped transitions.
1. Initial HTML Structure & Basic Styling
First, let’s set up our index.html and style.css files. Create an index.html file and a style.css file in the same directory.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Dashboard with Scoped View Transitions</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>My Dynamic Dashboard</h1>
<p>Experience smooth widget changes with Scoped View Transitions (experimental)</p>
<div class="controls">
<button id="swapWidgetsBtn">Swap Widget 1 & 2</button>
<button id="toggleWidget3Btn">Toggle Widget 3</button>
</div>
</header>
<main class="dashboard-grid">
<div class="widget" id="widget1" data-id="1">
<h2>Widget 1</h2>
<p>Sales Overview</p>
</div>
<div class="widget" id="widget2" data-id="2">
<h2>Widget 2</h2>
<p>Visitor Analytics</p>
</div>
<div class="widget" id="widget3" data-id="3">
<h2>Widget 3</h2>
<p>Recent Activity</p>
</div>
<div class="widget" id="widget4" data-id="4">
<h2>Widget 4</h2>
<p>Support Tickets</p>
</div>
</main>
<script src="script.js"></script>
</body>
</html>
Explanation of HTML:
- We have a
headerwith a title and two control buttons. - The
mainelement with classdashboard-gridwill be our scope for the transitions. This is the element on which we’ll callstartViewTransition(). - Inside
dashboard-grid, we have fourdiv.widgetelements. Each has a uniqueidanddata-idfor easy identification and manipulation.
Now, let’s add the basic styling for these elements. Create a style.css file in the same directory as your index.html.
style.css:
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f2f5;
color: #333;
}
header {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
text-align: center;
}
h1 {
color: #0056b3;
margin-bottom: 10px;
}
p {
line-height: 1.6;
}
.controls {
margin-top: 20px;
}
.controls button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 0 5px;
transition: background-color 0.2s ease;
}
.controls button:hover {
background-color: #0056b3;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
padding: 0 20px; /* Add some padding to the grid itself */
}
.widget {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
text-align: center;
transition: transform 0.3s ease; /* For hover effects, not the view transition itself */
min-height: 150px;
display: flex; /* To center content vertically */
flex-direction: column;
justify-content: center;
align-items: center;
}
.widget h2 {
color: #007bff;
margin-top: 0;
margin-bottom: 10px;
}
.widget p {
color: #555;
font-size: 0.9em;
}
/* Initially hide widget 3 if we want to toggle it in */
.widget[data-id="3"].hidden {
display: none;
}
Explanation of CSS:
- We’ve set up a basic responsive grid layout for our dashboard using
display: grid. - The
.widgetclass styles our individual cards, giving them a clean look. - We’ve added a
.widget[data-id="3"].hidden { display: none; }rule. This will be used by our JavaScript to hide/show widget 3, and the View Transition API will animate thisdisplaychange.
2. Preparing Widgets for Transition: view-transition-name
For the browser to know which elements to animate, we need to give them view-transition-names. These names act like unique identifiers for the browser during the transition. For our initial static setup, we can add them directly to the HTML using inline styles.
Update index.html: Add style="view-transition-name: widget-{{data-id}};" to each widget div.
<main class="dashboard-grid">
<div class="widget" id="widget1" data-id="1" style="view-transition-name: widget-1;">
<h2>Widget 1</h2>
<p>Sales Overview</p>
</div>
<div class="widget" id="widget2" data-id="2" style="view-transition-name: widget-2;">
<h2>Widget 2</h2>
<p>Visitor Analytics</p>
</div>
<div class="widget" id="widget3" data-id="3" style="view-transition-name: widget-3;">
<h2>Widget 3</h2>
<p>Recent Activity</p>
</div>
<div class="widget" id="widget4" data-id="4" style="view-transition-name: widget-4;">
<h2>Widget 4</h2>
<p>Support Tickets</p>
</div>
</main>
Explanation:
- Each widget now has a unique
view-transition-name, likewidget-1,widget-2, etc. This tells the browser that if an element withview-transition-name: widget-1exists before and after a DOM change within ourdashboard-gridscope, it should be animated.
3. Implementing the Swap Functionality with element.startViewTransition()
Now, let’s write the JavaScript to make our “Swap Widget 1 & 2” button work. This will demonstrate reordering elements within a specific scope. Create a script.js file in the same directory.
script.js:
// Feature detection for Scoped View Transitions
const isScopedViewTransitionsSupported = () => {
// Check if element.startViewTransition exists on a generic HTMLElement
return typeof HTMLElement.prototype.startViewTransition === 'function';
};
// Get references to our elements
const dashboardGrid = document.querySelector('.dashboard-grid');
const swapWidgetsBtn = document.getElementById('swapWidgetsBtn');
const toggleWidget3Btn = document.getElementById('toggleWidget3Btn');
const widget1 = document.getElementById('widget1');
const widget2 = document.getElementById('widget2');
const widget3 = document.getElementById('widget3');
// Initial check for Scoped View Transitions support
if (!isScopedViewTransitionsSupported()) {
console.warn('Scoped View Transitions are not supported in this browser or are behind a flag. Enable chrome://flags/#view-transitions-on-element in Chrome, or similar flags in other browsers.');
// Optionally, disable buttons or provide a fallback UI
swapWidgetsBtn.disabled = true;
toggleWidget3Btn.disabled = true;
swapWidgetsBtn.textContent += ' (Not Supported)';
toggleWidget3Btn.textContent += ' (Not Supported)';
}
// Function to swap widget 1 and widget 2
function swapWidgets() {
// Check if widgets exist
if (!widget1 || !widget2 || !isScopedViewTransitionsSupported()) return;
// The core of Scoped View Transitions:
// We call startViewTransition() on the *parent element* (dashboardGrid)
// where the DOM changes will occur.
const transition = dashboardGrid.startViewTransition(() => {
// This callback is where you perform your actual DOM updates.
// The browser takes the "old" snapshot *before* this callback runs,
// and the "new" snapshot *after* it completes.
console.log('Preparing to swap widgets 1 and 2...');
// Simple swap logic:
// Get the current parent of widget1 and widget2
const parent = widget1.parentNode;
// Create a temporary placeholder to correctly insert elements
const tempPlaceholder = document.createElement('div');
parent.insertBefore(tempPlaceholder, widget1);
// Move widget1 to widget2's position
parent.insertBefore(widget1, widget2);
// Move widget2 to widget1's original position (where tempPlaceholder is)
parent.insertBefore(widget2, tempPlaceholder);
// Remove the temporary placeholder
tempPlaceholder.remove();
console.log('DOM updated: Widgets 1 and 2 swapped.');
});
// You can optionally do something after the transition finishes
transition.finished.then(() => {
console.log('Swap transition finished!');
});
}
// Function to toggle the visibility of widget 3
function toggleWidget3() {
if (!widget3 || !isScopedViewTransitionsSupported()) return;
// Use the same dashboardGrid as the scope for this transition too!
// This demonstrates concurrent transitions if another one were active,
// though in this simple example, they won't overlap.
const transition = dashboardGrid.startViewTransition(() => {
// Toggle a 'hidden' class which sets display: none in CSS
widget3.classList.toggle('hidden');
console.log(`DOM updated: Widget 3 is now ${widget3.classList.contains('hidden') ? 'hidden' : 'visible'}.`);
});
transition.finished.then(() => {
console.log('Toggle widget 3 transition finished!');
});
}
// Attach event listeners to our buttons
swapWidgetsBtn.addEventListener('click', swapWidgets);
toggleWidget3Btn.addEventListener('click', toggleWidget3);
Explanation of JavaScript:
- Feature Detection: We start with
isScopedViewTransitionsSupported()to check ifHTMLElement.prototype.startViewTransitionexists. This is crucial for robust applications, allowing you to provide a fallback experience (like disabling buttons) if the feature isn’t available. - Element References: We grab our
dashboardGrid(our transition scope) and the buttons/widgets we’ll be interacting with. swapWidgets()Function:dashboardGrid.startViewTransition(() => { ... });This is the core. We callstartViewTransition()on thedashboardGridelement, which is the parent of the widgets we want to animate. The browser will take a snapshot of only thisdashboardGridsubtree.- Inside the callback, we perform the actual DOM manipulation: swapping
widget1andwidget2using standard DOM methods (insertBefore). - The browser automatically takes a snapshot of the
dashboardGridbefore the callback, then another after the callback. It then identifies elements with matchingview-transition-names within that scope and animates their positions, sizes, and opacities between the two states. transition.finished.then(...)allows us to execute code once the animation is complete.
toggleWidget3()Function:- Again,
dashboardGrid.startViewTransition(() => { ... });is used, demonstrating that multiple different types of changes can be animated within the same scope. - Inside, we simply toggle a
hiddenclass onwidget3. Our CSS (remember.widget[data-id="3"].hidden { display: none; }) will handle the actual visibility change. Thefade-inandfade-outkeyframes we’ll define next will apply because the element is appearing/disappearing.
- Again,
- Event Listeners: Finally, we attach our functions to the button clicks.
4. Enhancing Transitions with Custom Styling
The default animations for View Transitions are good, but we can make them even better! Let’s add some custom keyframes and target specific pseudo-elements to create a more engaging experience.
Update style.css: Add these rules to the end of your style.css file.
/*
Now, for the View Transition styles!
These will be applied by the browser during the transition.
Remember, these are global, but the elements they target
are defined by their `view-transition-name`.
*/
/* The pseudo-elements for the transition */
::view-transition-group(*) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
/* For elements that are moving/resizing */
::view-transition-image-pair(*) {
/* Ensure the old and new images are positioned correctly for movement */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Default fade-out for elements that are removed or hidden */
::view-transition-old(root) {
animation: fade-out 0.3s forwards;
}
/* Default fade-in for elements that are added or made visible */
::view-transition-new(root) {
animation: fade-in 0.3s forwards;
}
/* Custom animation for our widgets during a swap/move */
/* This targets the actual content of the moving widget, not just the image pair */
/* We can use transform to create a subtle bounce or scale effect */
::view-transition-old(widget-1),
::view-transition-old(widget-2),
::view-transition-new(widget-1),
::view-transition-new(widget-2) {
animation: widget-move-bounce 0.4s ease-in-out forwards;
}
@keyframes widget-move-bounce {
0% { transform: scale(1); }
50% { transform: scale(1.03); } /* Slightly enlarge during movement for a bouncy feel */
100% { transform: scale(1); }
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-20px); }
}
Explanation of CSS Updates:
- We’ve added
::view-transition-group(*)to apply a default animation duration and timing for all transitions. ::view-transition-image-pair(*)helps ensure that the browser correctly positions the “old” and “new” visual representations of moving elements.- We created a new keyframe
widget-move-bounceto give a subtle “pop” or “bounce” effect to our widgets as they move. - We applied this
widget-move-bouncespecifically to::view-transition-old(widget-1),::view-transition-new(widget-1), and similarly forwidget-2. This demonstrates how you can target specific elements within the transition using theirview-transition-name. - The
fade-inandfade-outanimations now also include atranslateYfor a subtle slide effect when elements appear/disappear. These will be used whenwidget3is toggled.
Now, open your index.html in a browser that supports (and has enabled the flag for) Scoped View Transitions! Click the buttons and observe the smooth, native-like animations. Notice how widget 1 and 2 glide to their new positions, and widget 3 gracefully fades and slides in or out.
Mini-Challenge: Add a New Widget Dynamically
You’ve seen how to swap and toggle. Now, let’s add a new widget dynamically and have it animate in smoothly!
Challenge:
Add a new button to your index.html and script.js that, when clicked, adds a brand new “Widget 5” to the dashboard-grid. This new widget should appear with a smooth fade-in animation, just like widget3 when it becomes visible.
Hint:
- HTML: Add a new
<button id="addWidget5Btn">Add Widget 5</button>in yourheader .controlsdiv. - JavaScript:
- Get a reference to this new button (
addWidget5Btn). - Create a new function, say
addWidget5(). - Inside
addWidget5(), ensure Scoped View Transitions are supported. - Use
dashboardGrid.startViewTransition()as your wrapper. - Within the
startViewTransitioncallback:- Create a new
divelement (document.createElement('div')). - Add classes (
widget), anid(widget5),data-id(5), and crucially, a uniquestyle="view-transition-name: widget-5;". - Add some content (e.g.,
<h2>Widget 5</h2><p>New Insights</p>). - Append it to the
dashboardGrid(dashboardGrid.appendChild(newWidget)).
- Create a new
- Attach an event listener to your new button (
addWidget5Btn.addEventListener('click', addWidget5)).
- Get a reference to this new button (
What to observe/learn:
You should see “Widget 5” appear with a smooth animation (using the fade-in keyframes you already defined), seamlessly integrating into the existing dashboard layout. This reinforces the power of Scoped View Transitions for adding brand new elements to a dynamic UI.
Common Pitfalls & Troubleshooting
Scoped View Transitions are powerful, but like any new API, they have their quirks. Here are some common issues you might encounter:
Forgetting
view-transition-nameor incorrect scoping: If an element isn’t animating, the first thing to check is itsview-transition-name.- Pitfall: Not setting
view-transition-nameon the elements you want to animate, or setting identical names for different elements within the same scope. - Troubleshooting: Ensure each element that you want to animate independently has a unique
view-transition-namewithin the scope of the element you callstartViewTransition()on. If you have two separatedashboardGridelements,widget-1could exist in both, but within a singledashboardGrid, they must be unique. - Pitfall: Calling
element.startViewTransition()on the wrong element (e.g.,document.bodyinstead ofdashboardGrid). - Troubleshooting: Always call
startViewTransition()on the immediate parent or an ancestor of the elements whose changes you want to animate. The browser will only look forview-transition-names within the subtree of the element on which the transition was started.
- Pitfall: Not setting
Browser Support & Flags:
- Pitfall: Your transitions aren’t working at all, even after following all steps.
- Troubleshooting: As of 2025-12-05, Scoped View Transitions are still experimental. Double-check that you’re using a browser that supports them (like Chrome Canary or Chrome Beta) and that you’ve enabled the necessary flag (
chrome://flags/#view-transitions-on-element). Without the flag,element.startViewTransitionwill simply be undefined, and yourif (!isScopedViewTransitionsSupported())check should catch this.
Complex Layout Changes &
display: none:- Pitfall: Elements appearing/disappearing with
display: nonedon’t animate as expected, or other elements “jump” around them. - Troubleshooting: When an element goes from
display: noneto visible (or vice-versa), the browser needs itsview-transition-namebefore thedisplaychange. Thefade-inandfade-outanimations we used are the correct approach. Ensure your CSS correctly targets::view-transition-old(root)and::view-transition-new(root)for these cases, or specificview-transition-names if you want custom fade effects. Also, remember thatgridandflexboxlayouts can sometimes cause elements around the transitioning item to reflow in ways that might not be perfectly smooth without careful CSS.
- Pitfall: Elements appearing/disappearing with
Performance with Many Elements:
- Pitfall: You have hundreds of items in a list, and animating all of them simultaneously causes jank.
- Troubleshooting: While Scoped View Transitions are optimized, animating a very large number of complex elements simultaneously can still be taxing. Consider strategies like “virtualization” (only rendering visible items) or limiting the number of elements with
view-transition-nameif performance becomes an issue. Test on lower-end devices to ensure a smooth experience for all users.
Summary
Congratulations! You’ve successfully delved into the bleeding edge of web animation with Scoped View Transitions.
Here’s a quick recap of what we covered:
element.startViewTransition(): The new method that allows you to initiate view transitions on a specific DOM subtree, rather than the entire document.- Targeted & Concurrent Animations: Scoped transitions enable multiple independent animations to run simultaneously, opening up new possibilities for dynamic UI elements like dashboard widgets, lists, and forms.
view-transition-namewithin Scope: Theview-transition-nameproperty remains essential for identifying elements across DOM changes, but its uniqueness is now considered within the scope of theelement.startViewTransition()call.- Practical Application: We built a dynamic dashboard, demonstrating how to animate widget reordering and visibility toggling with smooth, native-like transitions. You even tackled a mini-challenge to add a new widget dynamically!
- Experimental Status: We acknowledged that Scoped View Transitions are an experimental feature as of 2025-12-05, requiring browser flags for testing and careful consideration for production use.
You’ve taken a significant leap in your understanding of modern web animations. Scoped View Transitions represent a powerful tool for creating highly interactive and visually appealing user interfaces.
What’s next? In the next chapter, we’ll explore even more advanced techniques, such as animating attributes other than position and size, and integrating View Transitions with JavaScript animation libraries for truly bespoke effects. Keep experimenting, and keep building!