Introduction
Welcome back, future web animation wizard! In our previous chapters, you dipped your toes into the exciting world of the View Transition API, likely starting with document.startViewTransition() to create smooth, page-wide animations. That’s fantastic for full-page navigations, but what if you want to animate just a part of your page, perhaps an expanding card, a changing tab, or a component that updates its content with a delightful flourish?
That’s precisely what Scoped View Transitions are for, and in this chapter, we’re going to unlock their power using the incredible element.startViewTransition() method. You’ll learn how to initiate transitions within a specific DOM subtree, giving you granular control over your animations and enabling multiple, concurrent transitions on a single page. Get ready to make your web components truly come alive!
By the end of this chapter, you’ll not only understand the core concepts behind scoped transitions but also implement your very first one, turning static updates into fluid, engaging experiences. You should be comfortable with basic HTML, CSS, and JavaScript, and have a foundational grasp of the general View Transition API concepts we covered earlier. Let’s dive in!
Core Concepts: Bringing Transitions to Your Components
Before we start coding, let’s understand the magic behind element.startViewTransition().
What is element.startViewTransition()?
Imagine your web page is a grand theater stage. Up until now, using document.startViewTransition() was like changing the entire stage backdrop and all the props at once for a scene change. It’s powerful, but sometimes you just want to swap out a small prop or have a character perform a quick, localized action without affecting the whole stage.
element.startViewTransition() is your director’s cue for a mini-scene change within a specific area of your stage. Instead of calling it on the document, you call it on a particular HTMLElement. This element then becomes the “scope root” for that transition. Any visual changes that happen within this element (and its children) will be captured and animated, while the rest of the page remains untouched by this specific transition.
Why is this a game-changer?
- Component-level animations: You can encapsulate animations within individual UI components, making them more modular and reusable.
- Concurrent transitions: Unlike
document.startViewTransition()which only allows one transition at a time,element.startViewTransition()lets you run multiple independent transitions simultaneously on different parts of your page! This opens up a world of possibilities for rich, interactive UIs. - Performance benefits: By limiting the scope, the browser has less to analyze and animate, potentially leading to smoother transitions.
How Scoped Transitions Work (A Quick Recap & Expansion)
The fundamental mechanism is similar to document-scoped transitions:
- Snapshot: When
element.startViewTransition()is called, the browser takes a “snapshot” of the DOM within the specified element before any changes. - DOM Update: Your JavaScript code inside the
updateCallbackfunction (which you pass tostartViewTransition) then modifies the DOM within that same element. - New Snapshot: The browser takes a “new snapshot” of the DOM within the specified element after your changes.
- Pseudo-elements & Animation: The browser then creates pseudo-elements (
::view-transition-old(),::view-transition-new(),::view-transition-group(),::view-transition-image-pair()) for the elements that have aview-transition-nameand are changing within the scope. It then animates these pseudo-elements to create the visual transition.
The key difference? The scope! Only elements inside the element you call startViewTransition() on are considered for this particular animation.
Browser Support for Scoped View Transitions (as of 2025-12-05)
It’s important to remember that Scoped View Transitions are an exciting, experimental feature that is still under active development and specification by the WICG (Web Incubator Community Group).
As of December 5th, 2025:
- Chrome: Scoped View Transitions are available for developer testing in Chrome Canary / Dev versions (version ~130+) and typically require enabling an experimental flag.
- To enable: Navigate to
chrome://flagsin your Chrome Canary/Dev browser and search for “Experimental Web Platform features” or “Scoped View Transitions” and enable it.
- To enable: Navigate to
- Other Browsers: Support in other browsers (Firefox, Safari, Edge stable) is still pending or in early stages of implementation.
Why this matters: When developing with Scoped View Transitions, always include a feature detection check to ensure your users have a smooth experience, gracefully falling back to no-animation if the API isn’t supported.
Official References:
- WICG Scoped Transitions Proposal: https://github.com/WICG/shared-element-transitions/blob/main/scoped-transitions.md
- Chrome Developers Blog - “Ready for developer testing: Scoped view transitions”: https://developer.chrome.com/blog/scoped-view-transitions-feedback (Note: The date in the search results implies this was updated around 2025-09-24, making it highly relevant.)
Ready to build? Let’s go!
Step-by-Step Implementation: An Expanding Card
We’re going to create a simple card component that expands and collapses its content with a beautiful, scoped view transition.
Step 1: Basic HTML Structure
First, let’s set up our HTML file. Create an index.html and a style.css file.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scoped View Transition Card</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>My Awesome Scoped Card</h1>
<div class="card" id="myCard">
<div class="card-header">
<h2>Expandable Feature</h2>
<button id="toggleButton">Toggle Details</button>
</div>
<div class="card-content" id="cardContent">
<p>This is some initial content. Click the button to reveal more details about this fantastic feature!</p>
<p class="hidden-content">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Explanation:
- We have a main
divwith the classcardandid="myCard". This will be our scope root! - Inside, there’s a
card-headerwith a title and atoggleButton. - The
card-contentholds our text. Notice thehidden-contentparagraph – this is what we’ll reveal/hide. - We link
style.cssandscript.js.
Step 2: Basic CSS Styling
Now, let’s make it look decent and set up the hidden content.
/* style.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background-color: #f0f2f5;
color: #333;
}
h1 {
color: #2c3e50;
margin-bottom: 30px;
}
.card {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 600px;
padding: 25px;
margin: 20px;
overflow: hidden; /* Important for preventing content overflow during collapse */
border: 1px solid #e0e0e0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-header h2 {
margin: 0;
color: #34495e;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2980b9;
}
.card-content p {
margin-bottom: 10px;
line-height: 1.6;
}
/* Initially hide the extra content */
.hidden-content {
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out; /* For non-JS fallback */
}
/* Class to show the extra content */
.card.expanded .hidden-content {
max-height: 500px; /* A value larger than expected content height */
opacity: 1;
transition: max-height 0.5s ease-in, opacity 0.5s ease-in; /* For non-JS fallback */
}
Explanation:
- We’ve styled the
body,h1,card,card-header, andbuttonfor a clean look. - The
.hidden-contentis initially set tomax-height: 0andopacity: 0withoverflow: hiddento hide it. - When the
cardgets theexpandedclass,.hidden-contentwill show up. We added transitions here as a fallback, but the View Transition API will override these for the actual animation.
Step 3: JavaScript - Toggling Content (Without Transition)
Let’s get the toggle functionality working first, without any view transitions. Create a script.js file.
// script.js
const myCard = document.getElementById('myCard');
const toggleButton = document.getElementById('toggleButton');
const cardContent = document.getElementById('cardContent'); // We'll use this later
if (!myCard || !toggleButton || !cardContent) {
console.error("One or more required elements not found. Please check your HTML IDs.");
}
toggleButton.addEventListener('click', () => {
// Toggle the 'expanded' class on the card
myCard.classList.toggle('expanded');
// Update button text
if (myCard.classList.contains('expanded')) {
toggleButton.textContent = 'Hide Details';
} else {
toggleButton.textContent = 'Show Details';
}
});
Test it: Open index.html in your browser. Click the “Toggle Details” button. The content should appear and disappear abruptly.
Step 4: Introducing element.startViewTransition()
Now for the magic! We’ll wrap our DOM update in element.startViewTransition(). Remember to ensure you are running this in a browser that supports Scoped View Transitions (like Chrome Canary/Dev with flags enabled, as of 2025-12-05).
// script.js (modifying the event listener)
const myCard = document.getElementById('myCard');
const toggleButton = document.getElementById('toggleButton');
const cardContent = document.getElementById('cardContent');
if (!myCard || !toggleButton || !cardContent) {
console.error("One or more required elements not found. Please check your HTML IDs.");
}
toggleButton.addEventListener('click', () => {
// Feature detection for Scoped View Transitions
if (!myCard.startViewTransition) {
console.warn("Scoped View Transitions not supported in this browser/version, or flag not enabled.");
// Fallback to instant toggle
myCard.classList.toggle('expanded');
if (myCard.classList.contains('expanded')) {
toggleButton.textContent = 'Hide Details';
} else {
toggleButton.textContent = 'Show Details';
}
return; // Exit if not supported
}
// Start a scoped view transition on 'myCard'
myCard.startViewTransition(() => {
// This is the DOM update callback!
// All DOM changes here will be snapshotted and animated.
myCard.classList.toggle('expanded');
// Update button text
if (myCard.classList.contains('expanded')) {
toggleButton.textContent = 'Hide Details';
} else {
toggleButton.textContent = 'Show Details';
}
});
});
Explanation of changes:
- We added a feature detection
if (!myCard.startViewTransition). This is a best practice to ensure your code works gracefully even when the feature isn’t available. - The entire DOM update logic (toggling the
expandedclass and updating button text) is now wrapped insidemyCard.startViewTransition(() => { ... });. - Crucially, we call
startViewTransitiononmyCarditself, makingmyCardthe root of our scoped transition.
Test it: Reload index.html in your supported browser (Chrome Canary/Dev with flags). Click the button. You should now see a fading animation, but it might not be the smooth expansion we want yet. This is because we haven’t told the browser which specific elements to animate or how to animate them beyond the default cross-fade.
Step 5: Giving Elements view-transition-name
To get a proper expansion animation, we need to tell the browser which elements are “the same” before and after the DOM change. We do this with the view-transition-name CSS property.
Let’s add view-transition-name to our card-content and the toggleButton in style.css.
/* style.css (add these rules) */
.card-content {
/* Existing styles */
view-transition-name: card-content-area; /* NEW */
}
#toggleButton {
/* Existing styles */
view-transition-name: card-toggle-button; /* NEW */
}
Explanation:
view-transition-name: card-content-area;: This tells the browser to treat thecard-contentelement as a distinct entity for the transition. When itsmax-heightandopacitychange, the browser can animate between its “old” and “new” states.view-transition-name: card-toggle-button;: We’re also giving the button a name, so if we wanted to animate its text change or position, we could. For now, it just ensures it’s part of the snapshot.
Important Note: The view-transition-name must be unique within the current document for document-scoped transitions, but for scoped transitions, it needs to be unique within its parent’s scope. However, to be safe and avoid conflicts, it’s a good practice to keep them globally unique if possible, or at least unique within the component hierarchy.
Test it: Reload and click the button. You should now see a much more interesting animation! The content area should expand and collapse more smoothly, and the button might also have a subtle transition.
Step 6: Styling the View Transition Pseudo-elements
The browser provides default animations (usually a cross-fade), but we can customize them using CSS pseudo-elements.
/* style.css (add these rules at the end) */
/* The root of the scoped transition */
#myCard::view-transition {
/* You can apply general styles to the entire transition here if needed */
}
/* The group containing the old and new snapshots of the content area */
#myCard::view-transition-group(card-content-area) {
animation-duration: 0.5s; /* Make the transition a bit slower */
animation-timing-function: ease-in-out;
}
/* The old snapshot of the content area */
#myCard::view-transition-old(card-content-area) {
animation: fade-out 0.5s ease-in-out forwards;
/* We want the old content to fade out and potentially shrink */
/* opacity: 1; */ /* Default */
}
/* The new snapshot of the content area */
#myCard::view-transition-new(card-content-area) {
animation: fade-in 0.5s ease-in-out forwards;
/* We want the new content to fade in and potentially grow */
/* opacity: 0; */ /* Default */
}
/* Custom keyframe animations */
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-10px); }
}
/* For the button, let's just make it slide slightly */
#myCard::view-transition-group(card-toggle-button) {
animation-duration: 0.3s;
}
#myCard::view-transition-old(card-toggle-button) {
animation: button-slide-out 0.3s ease-in-out forwards;
}
#myCard::view-transition-new(card-toggle-button) {
animation: button-slide-in 0.3s ease-in-out forwards;
}
@keyframes button-slide-in {
from { transform: translateX(-10px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes button-slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(10px); opacity: 0; }
}
Explanation of new CSS:
#myCard::view-transition-group(card-content-area): This targets the container that holds both the old and new snapshots of thecard-content-area. We’re setting a general animation duration and timing function for this entire group.#myCard::view-transition-old(card-content-area): This targets the snapshot of thecard-contentbefore the DOM update. We apply afade-outanimation to it.#myCard::view-transition-new(card-content-area): This targets the snapshot of thecard-contentafter the DOM update. We apply afade-inanimation to it.- Notice the
#myCard::prefix. This is crucial! It tells the browser that these pseudo-elements belong to the view transition scoped to#myCard. If you omit#myCard::, these rules would apply to any view transition, including document-scoped ones, which might not be what you intend. - We defined custom
@keyframesforfade-in,fade-out,button-slide-in, andbutton-slide-outto create a more dynamic effect than a simple cross-fade. - We also added specific animations for the
card-toggle-buttonto show how different elements within the scope can have their own distinct animations.
Test it: Reload index.html one last time. Now, when you click the “Toggle Details” button, you should see a smooth expansion/collapse with fading and slight movement, all contained beautifully within our myCard component!
Mini-Challenge: Concurrent Scoped Transitions!
You’ve got one scoped transition working. Now, let’s explore the true power of element.startViewTransition(): concurrent transitions.
Challenge:
Add a small “Like” button and a counter inside our card-content. When the “Like” button is clicked, increment the counter and make just the counter perform a quick fade-in/out transition, while the main card expansion/collapse can still happen independently.
Steps to tackle this:
- HTML: Add a
divfor the like counter and button withincard-content. Give the counter element a uniqueid. - CSS: Style the like button and counter. Give the counter element a
view-transition-name. - JavaScript:
- Get a reference to the like button and counter.
- Add an event listener to the like button.
- Inside the like button’s event listener, use
counterElement.startViewTransition(() => { ... })to update the counter text. This is the key: a separatestartViewTransitioncall, scoped to the counter element itself. - Increment the counter value and update its
textContentinside the callback.
- CSS (View Transitions): Add specific
::view-transition-old()and::view-transition-new()styles for the counter’sview-transition-nameto define its animation (e.g., a subtle scale-up/down and fade).
Hint: Remember that view-transition-name needs to be unique for the element you want to transition. The counter element itself will be the scope root for its transition.
What to Observe/Learn:
- You should see the “Like” counter animate independently when clicked, even if the main card is also transitioning its expansion/collapse.
- This demonstrates how
element.startViewTransition()allows for truly independent and concurrent animations, making your UI feel incredibly dynamic and responsive.
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-name:- Symptom: Your scoped transition runs, but specific elements within the scope (like your
card-content) just pop in/out or only do a default cross-fade, even if you’ve defined custom animations for their pseudo-elements. - Reason: The browser needs
view-transition-nameto identify and snapshot an element as a distinct entity for the transition. Without it, the element is treated as part of its parent’s content, and the browser might not generate the specific::view-transition-old()/::view-transition-new()for it. - Fix: Ensure every element you want to animate distinctly has a unique
view-transition-nameCSS property (e.g.,view-transition-name: my-unique-id;).
- Symptom: Your scoped transition runs, but specific elements within the scope (like your
Calling
startViewTransition()on the Wrong Element:- Symptom: Your transition either affects the entire document when you expected it to be scoped, or it doesn’t happen at all.
- Reason: If you accidentally call
document.startViewTransition()instead ofmyElement.startViewTransition(), you’ll get a document-scoped transition. If you callstartViewTransition()on an element that doesn’t contain the elements you’re actually changing, then those changes won’t be captured within that scope. - Fix: Double-check that you’re calling
startViewTransition()on the precise parent element that should define the boundary of your transition.
DOM Changes Outside the
updateCallback:- Symptom: Your transition starts, but the elements don’t animate correctly, or you see a flicker before the animation.
- Reason: Only DOM changes that occur synchronously inside the
updateCallbackfunction passed tostartViewTransition()are included in the “new” snapshot. If you make changes before or after this callback, or asynchronously (e.g., in asetTimeoutoutside the callback), they won’t be part of the transition’s “new” state. - Fix: Ensure all the DOM manipulations you want to animate are performed within the
() => { ... }function passed tostartViewTransition().
Browser Support & Flags:
- Symptom: Nothing happens, or you see the
console.warnabout unsupported features. - Reason: As discussed, Scoped View Transitions are experimental. They aren’t in stable versions of all browsers yet.
- Fix: Always use feature detection (
if (myElement.startViewTransition)) and test in a compatible browser (Chrome Canary/Dev as of 2025-12-05) with the necessary experimental flags enabled.
- Symptom: Nothing happens, or you see the
Summary
Phew! You’ve just created your very first Scoped View Transition. That’s a huge step towards building highly interactive and visually appealing web interfaces!
Here are the key takeaways from this chapter:
element.startViewTransition(): This method allows you to initiate view transitions within a specific DOM subtree, making the targetelementthe “scope root” of the animation.- Component-Level Control: Scoped transitions are perfect for animating individual components, giving you granular control over UI changes.
- Concurrent Animations: A major advantage is the ability to run multiple
element.startViewTransition()animations simultaneously on different parts of the page. view-transition-name: Remains crucial for identifying elements to be snapshotted and animated within the scope.- Scoped Pseudo-elements: You style scoped transitions using pseudo-elements like
#myElement::view-transition-group(name),#myElement::view-transition-old(name), and#myElement::view-transition-new(name), ensuring your CSS applies only to that specific scope. - Experimental Feature: Remember that Scoped View Transitions are still experimental (as of 2025-12-05) and require feature detection and a compatible browser (Chrome Canary/Dev with flags) for testing.
You now have the power to create localized, elegant animations that can dramatically enhance user experience. In the next chapter, we’ll dive deeper into more advanced styling techniques and explore how to orchestrate multiple concurrent transitions for even more complex interactions. Keep up the amazing work!