Skip to content

View Transitions API ​

2025.07.04#learning-lessons

The View Transition API in CSS is relatively new and is a great tool for animating elements that trigger immediate visibilty changes from block to none and vice-versa. Click on the quadrants below to see an example:

Each quadrant triggers an animation to display a scaled up version of the quadrant over the top of the grid. This is akin to an image gallery but simplified into this Micrsoft-Simon-esque looking object. Full demo on Codepen

HTML Setup ​

HTML
<div id="view-transition-gallery">
    <main id="grid">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
    </main>
    <main id="zoom">
    </main>
</div>

🔑 points:

  • #view-transition-gallery helps contain the below elements.
  • #grid contains the 4 colored quadrants.
  • #zoom is the 'scaled up' gallery item appearing after clicking a quadrant color.

The #grid quadrants ​

CSS
#grid {
    display: grid;
    width: var(--size);
    height: var(--size);
    grid-template-columns: auto auto;
    grid-template-rows: auto auto;
    gap: 10px;
}

#grid div:nth-child(1) {
    background-color: #ffd1dc;
}
...
/* :nth-child() background-color definition for every other div */
/* ...more styles for box shadows and hovering */

🔑 points:

The goal is to set up a 2x2 grid with the following:

  • width and height on #grid use the same var(--size) value (200px in this case) to keep things square in its overall size
  • grid-template-columns and grid-template-rows each use 2 auto values to ensure the div elements orient themselves as a 2x2 grid.
  • Coloring each quadrant is a result of the :nth-child(<number>) rule that continues for each div

The #zoom element ​

CSS
#zoom {
    display: none;
    height: calc(var(--size) * 0.8);
    width: calc(var(--size) * 0.8);
    margin: calc(var(--size) * 0.1);
    view-transition-name: zoom-transition;
    box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.2);
}
  • This element is what gets scaled up (acting like an image gallery element). Note that it starts with display:none
  • It doesn't completely cover the 4 quadrant squares when scaled up due to the calc(...) statements. This lets you click on the outer edges of the visible colored quadrants to bring in a different zoomed quadrant.
  • view-transition-name: zoom-transition is probably the most important piece of this. It establishes a selector for CSS to look for when deciding what elements to take "snapshots" of to prepare for the before and after states as animations happen.

View Transition (Where the magic 🪄 happens) ​

CSS

::view-transition-new(zoom-transition)
    { animation: zoom-in 0.5s ease-in-out; }
::view-transition-old(zoom-transition)
    { animation: zoom-out 0.5s ease-out; }

@keyframes zoom-in {
    from { transform: scale(0); }
    to { transform: scale(1); }
}

@keyframes zoom-out {
    from { transform: scale(1); }
    to { transform: scale(0); }
}

🔑 points:

  • Both ::view-transtion-new(zoom-transition) and ::view-transition-old(zoom-transition) are selectors tied to the view-transition-name: zoom-transition property/value from the #zoom element.
  • Think of the 'new' in the pseudo-selector ::view-transition-new(zoom-transition) as what happens when the #zoom element changes from display:none to display:block. When this transition happens from none to block CSS selects applies the zoom-in animation which is now connected to the #zoom element.
  • The opposite of this is the 'old' in the psuedo-selector ::view-transition-old(zoom-transition) which is going to select the #zoom element again and kick off the zoom-out animation as we go from a display:block to display:none

A dash of Javascript ​

We do need a bit of Javascript to change the display state of the #zoom element from none to block (and vice-versa), which is done with this Javascript:

Javascript
document.getElementById("grid").addEventListener('click', async (el) => {
    const transition = document.startViewTransition(() => {
        if (el) {
            const color = getComputedStyle(el.target).backgroundColor;
            const zoomEl = document.getElementById("zoom");
            zoomEl.style.display = "block";
            zoomEl.style.backgroundColor = color;
        }
    })
    await transition.ready;
})

document.getElementById("zoom").addEventListener('click', async (e) => {
    const transition = document.startViewTransition(() => {
        e.target.style.display = "none";
    });
    await transition.ready;
});

🔑 points:

  • We add a click listener on the #grid element that will grab the #zoom element and do 2 things:
    • Make the element visible with zoomEl.style.display="block"
    • Also take the color from the quadrant that was clicked and apply it to the #zoom element
  • Note that transition stores the document.startViewTransition function which immediately fires its callback that modifies the DOM. This transition is await-ed so that we can let the animation complete without interruption.
  • We do have click functionality on the #zoom element as well which modifies its display value to none and also awaits the transition.

Without the transition ​

For comparison sake, lets look at what the experience would be like without the transition. Which do you like better ? 😁

~ Zano