view-transition-class

Sunkanmi Fafowora on

The CSS view-transition-class property allows multiple elements with different view-transition-name values to share the same animation styles by grouping them under a common class identifier. In other words, multiple named elements in a view transition can be selected together to share styles.

.element {
  view-transition-name: bear;
  view-transition-class: bearhugs;
}

::view-transition-group(.bearhugs) {
  animation-duration: 500ms;
}

So, in the following demo, clicking on the element triggers a view transition that is given a transition class that is individually applied to the transition group, the old transition state, and the new transition state.

Specifically, using view-transition-class, we can apply the same styles to different elements participating in a view transition in any of the related pseudo-elements (::view-transition-group(), ::view-transition-image-pair(), ::view-transition-old(), ::view-transition-new()).

Note that, to differentiate it from regular names, we precede it with a dot (.), just like a regular CSS class, e.g. .cool-transition instead of cool-transition.

The view-transition-class property is defined in the CSS View Transitions Module Level 2 specification.

Syntax

view-transition-class: none | <custom-ident> +;
  • Values: none | <custom-ident>+
  • Initial: none
  • Applies to: all elements
  • Inherited: no
  • Computed value: as specified
  • Animation type: discrete

Values

/* It can be a <custom-ident> */
view-transition-class: card;
view-transition-class: Box;
view-transition-class: popUp1;
view-transition-class: avatar-icon;

/* or a space-separated list */
view-transition-class: item gallery custom;

Where…

  • none (default): The element doesn’t have any class.
  • <custom-ident> +: a space-separated list of unique case-sensitive identifiers used to identify the view transition class. For example, mini, JOLLOF, Rice, etc. It cannot be an existing CSS global keyword, like none, initial, reset, inherit, or all.

Basic usage

First things first. We register a view transition on an element by assigning it a  view-transition-name:

.element-1 {
  view-transition-name: cool-transition-1;
}

Let’s say we have another element on the page and it gets its own name:

.element-1 {
  view-transition-name: cool-transition-1;
}

.element-2 {
  view-transition-name: cool-transition-2;
}

Cool. Even though those two view transitions might be doing different things, we might still want them to share some similarities, for example, to run for two seconds. We could do this:

::view-transition-group(cool-transition-1) {
     animation-duration: 2s;
}

::view-transition-group(cool-transition-2) {
     animation-duration: 2s;
}

Or, we could “link” the two transitions together using the view-transition-class property, giving them a shared name:

.element-1 {
  view-transition-name: cool-transition-1;
  view-transition-class: cool-transition;
}

.element-2 {
  view-transition-name: cool-transition-2;
  view-transition-class: cool-transition;
}

Now we can select both elements with this view transition class using the ::view-transition-group pseudo-element. We declare the pseudo and reference the view-transition-class name we used (which is .cool-transition in this example):

::view-transition-group(.cool-transition) {
  animation-duration: 2s;
}

This way, we can share styles between the two elements in the same place. You might not think that’s all that impressive. After all, we could have used a chained selector to do the same thing:

::view-transition-group(cool-transition-1),
::view-transition-group(cool-transition-2) {
  animation-duration: 2s;
}

But that gets hairy once we start working with multiple transitions that we want to group together. What we can do is share more than one view transition class at a time on a single element as long as we separate the names with spaces (no commas):

.element-2 {
  view-transition-name: cool-transition-2;
  view-transition-class: cool-transition another-transition and-another-one;
}

And we could select them with the class names the same way we did before to share specific styles with the group:

::view-transition-group(.cool-transition) {
  animation-duration: 2s;
}

::view-transition-group(.another-transition) {
  animation-delay: .5s;
}

::view-transition-group(.and-another-one) {
  animation-direction: alternate;
}

Why we need this

The benefit of using view-transition-class becomes even clearer when you have an entire series of view transitions on the same page. Each element on the page needs a unique view-transition-name even if they do the same thing. So, let’s assume we have a series of card components on the page:

<div class="cards">
  <div class="card" id="card-1"></div>
  <div class="card" id="card-2"></div>
  <div class="card" id="card-3"></div>
  <!-- .etc. -->
  <div class="card" id="card-25"></div>
</div>

Rather than handwrite a view-transition-name for each and every one of those cards (i.e., .card-1, .card-2, etc.), we can generate those names using the attr() function:

/* Select all .card elements with an id */
.card[id] {
  /* card-1, card-2, card-3, ... */
  view-transition-name: attr(id type(<custom-ident>), none);
}

…and use view-transition-class to make sure they all share the same animation styles:

/* Select all .card elements with an id */
.card[id] {
  /* card-1, card-2, card-3, ... */
  view-transition-name: attr(id type(<custom-ident>), none);
  view-transition-class: card;
}

That’s a lot less code to write and things to name (which is always hard), at least in theory. Just know that using the attr() function this way has limited browser support. For now, you’ll need to set unique names manually or use JavaScript.

Example: Same-page transitions

There are two types of view transitions: same page and multi-page transitions (also referred to as same-document and cross-document transitions). A same-page transition is where the transition does not trigger a page reload. Conversely, a multi-page transition is where the transition is part of navigating from one page to another.

The last example we looked at is a multi-page transition: click on one card component to navigate to another page. But we can use the view-transition-class property to transition elements on the same page.

Imagine we want to toggle the visibility boxes on a page by clicking buttons. There are two divs — each with a .box class and unique IDs — and two buttons — each with its own unique ID. Let’s give one of those boxes a .hidden class because we don’t want that to show in our demo just yet.

<button id="btnA">Show/Hide Box A</button>
<button id="btnB">Show/Hide Box B</button>

<div id="boxA" class="box">Box A</div>
<div id="boxB" class="box hidden">Box B</div>

Next, we apply a unique view-transition-name to each box:

#boxA {
  view-transition-name: boxA;
}

#boxB {
  view-transition-name: boxB;
}

…and give them just one view-transition-class called myBox. Giving them unique view transition classes kind of defeats the whole purpose, right?

#boxA,
#boxB {
  view-transition-class: myBox;
}

Finally, we pass the myBox view transition class to the ::view-transition-group() pseudo-element. This way, we can apply an animation to both named elements. Under the hood, we are applying the same animation to both the ::view-transition-old() and ::view-transition-new() pseudo-elements and their associated ::view-transition-image-pair().

/* style snapshots when animating */
::view-transition-group(.myBox) {
  animation-duration: 0.8s;
}

Next, we can apply a fadeIn animation to the view transition class myBox for the new state using ::view-transition-new().

::view-transition-new(.myBox) {
  animation: fadeIn both;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

And a fadeOut animation using ::view-transition-old().

::view-transition-old(.myBox) {
  animation: fadeOut both;
}

@keyframes fadeOut {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-20px);
  }
}

We still need JavaScript to trigger our view transitions. First, we’ll define variables for all of elements we want to select:

const boxA = document.querySelector("#boxA");
const boxB = document.querySelector("#boxB");

const btnA = document.querySelector("#btnA");
const btnB = document.querySelector("#btnB");

We’ll be calling document.startViewTransition() on the boxes when any of the buttons are clicked. This way, whenever #btnA is clicked, any changes to #boxA and #boxB will trigger the animation we defined in CSS.

btnA.addEventListener("click", () => {
  document.startViewTransition(() => {
    boxA.classList.toggle("active");
    boxA.classList.toggle("hidden");
    boxB.classList.toggle("hidden");
  });
});

btnB.addEventListener("click", () => {
  document.startViewTransition(() => {
    boxB.classList.toggle("active");
    boxB.classList.toggle("hidden");
    boxA.classList.toggle("hidden");
  });
});

The elements will animate following the animations we defined on each view transition pseudo-element. It all comes together in the next animation:

Example: Multi-page transitions

When navigating between pages, you typically want the old page to fade out while the new page slides in. The view-transition-class property lets you apply these animations consistently across all pages. Define the class on elements that appear on multiple pages:

/* Shared across all pages */
.card {
  view-transition-name: hero;
  view-transition-class: card-transition;
}

Then style the outgoing and incoming states differently:

/* Page leaving */
::view-transition-old(.card-transition) { 
  animation: fade-out 0.3s ease-out;
}

/* Page entering */
::view-transition-new(.card-transition) { 
  animation: slide-in 0.3s ease-out;
}

@keyframes fade-out {
  to { opacity: 0; }
}

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

This creates a smooth transition where the old page fades away as the new page slides into view.

Note: Cross-document view transitions currently require opt-in via the @view-transition at-rule in your CSS or a meta tag in your HTML.

Specificity

When targeting elements inside view transition pseudo-elements, we also have a concept of specificity, as in regular CSS selectors. However, contrary to intuition, names and classes defined through view-transition-name and view-transition-class have a specificity score of (0, 0, 1):

div {
  view-transition-name: mango;
  view-transition-class: supermangos;
}

/* Specificity: 0,0,1 */
::view-transition-group(.supermangos) { /* ... */ }

/* Specificity: 0,0,1 */
::view-transition-group(mango) { /* ... */ }

So, which one wins? The transition class is declared after the transition name on the div element, but they are applied in opposite order on the groups. If we were looking only at the div element, we’d assume that the transition class wins because it is lower in the CSS cascade.

But really it’s the transition name that wins because it is declared on the last transition group, which is lower in the CSS cascade. The key rule: when both target the same element, the last declaration in your stylesheet wins, regardless of whether it uses a name or class. This might seem counterintuitive since they’re declared in different places (on the element vs. on the pseudo-element), but the cascade follows the pseudo-element declarations.

And while we’re at it, it’s worth noting that there’s no world in which we can mix transition names (i.e., mango) and classes (i.e., .supermangos) together on a transition group:

/* Nope! */
::view-transition-group(.supermangos mango) { /* ... */ }

Specification

The CSS view-transition-class property is defined in the CSS View Transitions Module Level 2 specification. The specification is an Editor’s Draft, meaning it’s been widely reviewed and is intended to become an official W3C Recommendation, but is still subject to change.

Browser support

And we can check for support in JavaScript:

if (!document.startViewTransition) {
  // Fallback behavior
}