Welcome back, intrepid web developer! In our previous chapters, you learned the magic of initiating Scoped View Transitions using element.startViewTransition(). You saw how effortlessly the browser can animate changes within a specific part of your page, creating delightful user experiences. But what if you want more control? What if you want to dictate how those animations happen?

That’s exactly what we’ll uncover in this chapter! We’re going to peel back the layers and peer into the inner workings of a View Transition. You’ll learn about the special “pseudo-elements” the browser creates behind the scenes to perform these animations. Understanding these elements is your key to unlocking truly custom, beautiful, and performant transitions. Get ready to dive deep into the browser’s rendering process and gain mastery over your animations!

Before we begin, make sure you’re comfortable with basic HTML, CSS, and JavaScript, and that you’ve completed the exercises from the previous chapters, especially Chapter 3 where we introduced element.startViewTransition().

The Browser’s Little Helpers: View Transition Pseudo-elements

When you trigger a View Transition, the browser doesn’t just magically swap out content. Instead, it performs a clever dance involving snapshots and specially crafted pseudo-elements. Think of it like a movie director: before changing a scene, the director takes a “before” picture, sets up the “after” scene, and then uses special effects to blend them together.

Here’s a simplified breakdown of what happens:

  1. Snapshot of the “Old” State: Just before your DOM changes (or immediately when startViewTransition is called), the browser takes a snapshot of the relevant part of the page (the “old” view).
  2. DOM Update: Your JavaScript code runs, updating the DOM to its “new” state.
  3. Snapshot of the “New” State: The browser then takes a snapshot of the new state of the DOM.
  4. Pseudo-element Creation: Instead of showing the new DOM immediately, the browser creates a temporary overlay using a set of CSS pseudo-elements. These pseudo-elements represent the “old” and “new” snapshots, and it’s these elements that are animated.
  5. Animation: CSS animations are applied to these pseudo-elements to transition from the old snapshot to the new snapshot.
  6. Cleanup: Once the animation is complete, the pseudo-elements are removed, and the live “new” DOM is displayed.

The beauty of this approach is that the animations happen on a separate layer, preventing layout shifts and ensuring smooth performance.

The View Transition Tree

Let’s look at the specific pseudo-elements the browser creates. They form a structure that you can inspect and style. When a View Transition is active, you’ll find something like this in your browser’s developer tools (though you won’t see them directly in your HTML):

::view-transition
  ::view-transition-group(root)
    ::view-transition-image-pair(root)
      ::view-transition-old(root)
      ::view-transition-new(root)

This is the default structure for a simple, page-level transition. For Scoped View Transitions, the (root) name will be replaced by the specific view-transition-name you assign to the element you’re transitioning, or a browser-generated one if not specified.

Let’s break down each piece:

  • ::view-transition: This is the outermost container for the entire transition. It covers the whole viewport and acts as the canvas where the animation plays out. You can style this to change the background or add global transition effects.
  • ::view-transition-group(<name>): This pseudo-element groups the “old” and “new” snapshots of a specific element. The <name> here comes from the view-transition-name CSS property (which we’ll introduce shortly!). If you don’t provide a name, the browser will generate one for you, or use root for a document-level transition.
  • ::view-transition-image-pair(<name>): Inside the group, this element holds the actual “image” snapshots. It acts as a wrapper for the ::view-transition-old and ::view-transition-new elements.
  • ::view-transition-old(<name>): This is the snapshot of the element before the DOM change. It’s often animated to fade out or move away.
  • ::view-transition-new(<name>): This is the snapshot of the element after the DOM change. It’s typically animated to fade in or move into place.

Are you starting to see the power here? By targeting these specific pseudo-elements with CSS, you can control their animations!

The view-transition-name Property: Your Animation Handle

The key to telling the browser which elements you want to animate independently (and therefore get their own ::view-transition-group and ::view-transition-image-pair) is the view-transition-name CSS property.

  • What it is: view-transition-name is a CSS property you apply to an HTML element. It takes a unique identifier as its value.
  • Why it’s important: It tells the browser, “Hey, this element is special! Please track its visual state before and after the DOM change, and create dedicated pseudo-elements for it during a View Transition.” Without this, elements will simply fade in and out as part of the overall page transition.
  • How it works: When you apply view-transition-name: my-unique-id; to an element, the browser will create ::view-transition-group(my-unique-id), ::view-transition-image-pair(my-unique-id), ::view-transition-old(my-unique-id), and ::view-transition-new(my-unique-id) for that specific element during a transition. This allows you to style only that element’s transition.

Crucial Rule: Every view-transition-name must be unique on the page at any given time. If two elements have the same view-transition-name at the same time, only one will be transitioned. If an element gains or loses its view-transition-name during a transition, that’s perfectly fine and part of the magic!

Step-by-Step Implementation: Customizing Our Scoped Transition

Let’s put this knowledge into practice. We’ll start with a simple example and progressively add view-transition-name and custom CSS to see the pseudo-elements in action.

First, let’s set up our basic HTML and CSS. Create an index.html file 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 Transitions: Pseudo-elements</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Scoped View Transitions</h1>
        <div class="card-wrapper">
            <div id="card1" class="card active">
                <h2>Card One</h2>
                <p>This is the first card. Click the button to toggle!</p>
                <button onclick="toggleCards()">Toggle Cards</button>
            </div>
            <div id="card2" class="card">
                <h2>Card Two</h2>
                <p>Hello from the second card! It's currently hidden.</p>
                <button onclick="toggleCards()">Toggle Cards</button>
            </div>
        </div>
    </div>

    <script>
        function toggleCards() {
            const card1 = document.getElementById('card1');
            const card2 = document.getElementById('card2');

            // Check for browser support for Scoped View Transitions (element.startViewTransition)
            if (!card1.startViewTransition) {
                console.warn("Scoped View Transitions not supported in this browser.");
                // Fallback: just toggle classes without transition
                card1.classList.toggle('active');
                card2.classList.toggle('active');
                return;
            }

            // Start the Scoped View Transition on the card-wrapper
            card1.startViewTransition(() => {
                card1.classList.toggle('active');
                card2.classList.toggle('active');
            });
        }
    </script>
</body>
</html>

style.css:

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    display: flex;
    justify-content: center;
    align-items: flex-start; /* Align to top for better h1 visibility */
    min-height: 100vh;
    background-color: #f0f2f5;
    margin: 0;
    padding-top: 50px; /* Add some space at the top */
}

.container {
    text-align: center;
    max-width: 600px;
    width: 100%;
    padding: 20px;
}

h1 {
    color: #333;
    margin-bottom: 40px;
}

.card-wrapper {
    position: relative; /* Needed for absolute positioning of cards if desired, or for z-index */
    min-height: 250px; /* Give it some height so content doesn't collapse */
    display: flex; /* Use flexbox to center cards */
    justify-content: center;
    align-items: center;
    border-radius: 12px;
    background-color: #fff;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
    padding: 20px;
}

.card {
    background-color: #ffffff;
    border-radius: 8px;
    padding: 25px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
    text-align: left;
    width: 90%; /* Make cards take up most of the wrapper width */
    max-width: 400px;
    transition: opacity 0.3s ease-in-out; /* Fallback transition */
    position: absolute; /* Position cards absolutely within wrapper */
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    opacity: 0; /* Hidden by default */
    pointer-events: none; /* Make hidden cards unclickable */
}

.card.active {
    opacity: 1;
    pointer-events: all; /* Make active card clickable */
}

.card h2 {
    color: #007bff;
    margin-top: 0;
    margin-bottom: 15px;
}

.card p {
    color: #555;
    line-height: 1.6;
    margin-bottom: 25px;
}

.card button {
    background-color: #007bff;
    color: white;
    border: none;
    padding: 12px 25px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 1rem;
    transition: background-color 0.2s ease;
}

.card button:hover {
    background-color: #0056b3;
}

Open index.html in a modern browser (like Chrome 111+ or Edge 111+ for document.startViewTransition, and Chrome 126+ for element.startViewTransition as of late 2024/early 2025). You should see “Card One”. Clicking the “Toggle Cards” button will make “Card Two” appear and “Card One” disappear with a smooth cross-fade, thanks to the default View Transition behavior.

Step 1: Assigning view-transition-name

Right now, the entire card-wrapper content is treated as a single “thing” transitioning. Let’s make the card titles transition independently. We’ll give them a view-transition-name.

Modify style.css: Add this rule:

/* ... existing CSS ... */

.card h2 {
    color: #007bff;
    margin-top: 0;
    margin-bottom: 15px;
    view-transition-name: card-heading; /* Add this line! */
}

Now, refresh your page and try toggling the cards. Did you notice a difference? The default transition for the heading is likely a simple cross-fade, but now the browser knows to track this specific heading element across the transition.

Step 2: Targeting and Customizing Pseudo-elements

With view-transition-name: card-heading applied, the browser creates ::view-transition-old(card-heading) and ::view-transition-new(card-heading) during the transition. We can now target these specifically!

Let’s make the old heading slide out to the left and fade, while the new heading slides in from the right and fades.

Modify style.css: Add these CSS rules after your existing .card h2 rule:

/* ... existing CSS ... */

/* Custom animations for the card headings */
::view-transition-old(card-heading) {
    animation: slide-out-left 0.6s ease-out forwards;
}

::view-transition-new(card-heading) {
    animation: slide-in-right 0.6s ease-out forwards;
}

/* Define the keyframe animations */
@keyframes slide-out-left {
    from {
        opacity: 1;
        transform: translateX(0);
    }
    to {
        opacity: 0;
        transform: translateX(-100%);
    }
}

@keyframes slide-in-right {
    from {
        opacity: 0;
        transform: translateX(100%);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

Save your style.css and refresh index.html. Now, when you toggle the cards, observe the headings! They should slide out and in beautifully, independently of the rest of the card content which still uses the default cross-fade. Pretty neat, right?

Step 3: Customizing the Default Page Transition

What about the rest of the content that doesn’t have a view-transition-name? That content falls under the default (root) transition. We can customize this too!

Let’s make the entire old view fade out faster and the new view fade in slower.

Modify style.css: Add these rules:

/* ... existing CSS ... */

/* Custom animations for the default (root) transition */
::view-transition-old(root) {
    animation: fade-out-fast 0.3s ease-in forwards;
}

::view-transition-new(root) {
    animation: fade-in-slow 0.8s ease-out forwards;
}

/* Define keyframes for root transition */
@keyframes fade-out-fast {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes fade-in-slow {
    from { opacity: 0; }
    to { opacity: 1; }
}

Now, refresh and toggle again. You’ll see the card-heading animations still doing their slide, but the rest of the content (the paragraphs, buttons, and card backgrounds) will use the new fade-out-fast and fade-in-slow animations.

This demonstrates the hierarchical nature of View Transitions: specific view-transition-name elements get their own animation rules, and everything else falls back to the (root) rules.

Mini-Challenge: Elevating a Button’s Transition

Your turn! Let’s take what you’ve learned and apply it to another element.

Challenge: Make the “Toggle Cards” button itself have a distinct animation. When Card One is active, the button should shrink and fade out. When Card Two appears, its button should grow and fade in.

Hint:

  1. Assign a view-transition-name to the <button> elements. Remember, they need to be unique at any given time. You can assign the same name, as only one button will be active at a time.
  2. Create custom @keyframes for a “shrink-fade-out” and a “grow-fade-in” effect.
  3. Apply these animations to ::view-transition-old(your-button-name) and ::view-transition-new(your-button-name).

What to observe/learn: How view-transition-name allows you to isolate and animate specific UI components, even when they’re inside a larger transitioning area.

(Take a moment, pause, and try to implement this challenge yourself!)

Solution (Don’t peek until you’ve tried!):

First, add view-transition-name to the buttons in style.css:

/* ... existing CSS ... */

.card button {
    /* ... existing button styles ... */
    view-transition-name: toggle-button; /* Add this! */
}

Then, add the custom pseudo-element styling and keyframes:

/* ... existing CSS ... */

/* Custom animations for the toggle button */
::view-transition-old(toggle-button) {
    animation: shrink-fade-out 0.4s ease-in forwards;
}

::view-transition-new(toggle-button) {
    animation: grow-fade-in 0.4s ease-out forwards;
}

/* Keyframes for button animation */
@keyframes shrink-fade-out {
    from {
        opacity: 1;
        transform: scale(1);
    }
    to {
        opacity: 0;
        transform: scale(0.8);
    }
}

@keyframes grow-fade-in {
    from {
        opacity: 0;
        transform: scale(0.8);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

Now, when you toggle, your headings slide, the main content fades, and the buttons shrink/grow beautifully!

Common Pitfalls & Troubleshooting

  1. Forgetting view-transition-name: If an element isn’t animating independently, or its transition looks like the default (root) one, chances are you forgot to apply view-transition-name to it. Remember, only elements with a unique view-transition-name get their own ::view-transition-old() and ::view-transition-new() pseudo-elements.
  2. Duplicate view-transition-name: If you have two elements with view-transition-name: same-name; simultaneously active on the page, only one of them will participate in the transition. The browser needs unique identifiers. This is less of an issue when elements hide/show, but critical if they are both present and changing.
  3. Overly Complex Animations: While powerful, don’t overdo it! Too many complex animations, especially on large elements, can lead to performance issues. Keep your keyframes simple and focused. Use browser developer tools (Performance tab) to profile your animations if you notice jank.
  4. Browser Support: As of 2025-12-05, document.startViewTransition is widely supported in Chromium-based browsers (Chrome, Edge, Opera, Brave), and has good support in Firefox. element.startViewTransition (scoped transitions) is newer but also gaining traction, particularly in Chromium-based browsers. Always include a feature detection check like if (!element.startViewTransition) to provide a graceful fallback for users on older browsers or those that don’t yet support the API. You can check the latest compatibility on MDN Web Docs: View Transition API.
  5. Debugging Pseudo-elements: You can inspect these pseudo-elements in Chrome DevTools! Go to the “Elements” tab, and when a View Transition is active, look for the ::view-transition tree at the very top of the DOM. You can select these elements and see their computed styles and animations. This is incredibly helpful for debugging your custom CSS.

Summary

Congratulations! You’ve successfully navigated the intricate anatomy of Scoped View Transitions. Here are the key takeaways from this chapter:

  • View Transitions work by taking snapshots of the “old” and “new” states of your UI.
  • These snapshots are then rendered as special CSS pseudo-elements: ::view-transition, ::view-transition-group(<name>), ::view-transition-image-pair(<name>), ::view-transition-old(<name>), and ::view-transition-new(<name>).
  • The view-transition-name CSS property is crucial for identifying specific elements you want to animate independently. Its value must be unique on the page at any given time.
  • By targeting these pseudo-elements with standard CSS animation properties and @keyframes rules, you gain granular control over how your transitions look and feel.
  • Elements without a view-transition-name fall back to the default (root) transition rules.
  • Always include feature detection for startViewTransition and be mindful of performance and uniqueness of view-transition-name.

You now have the power to not just initiate transitions, but to sculpt them with CSS! In the next chapter, we’ll explore even more advanced customization techniques, including how to handle complex layouts and potential accessibility considerations. Keep up the fantastic work!