How To Use The View Transition Pseudo-element Tree

How To Use The View Transition Pseudo-element Tree
Photo by Pawel Czerwinski / Unsplash

Modern Web Weekly #20

When you use View Transitions inside your web app, the browser will construct a pseudo-element tree that holds the old and new states and takes care of the animation between them.

Here's what that tree looks like:

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

Usually you will only deal with the two bottom ones ::view-transition-old(root) and ::view-transition-new(root) which are the ones that hold the old and new state respectively. But the other ones can also be useful if you know how to use them right.

Personally, I hadn't found a use case for these before until I decided to add a View Transitions demo to What PWA Can Do Today. In this demo, you can choose from four transitions on a mobile device that will be applied to the app. One of these transitions is a card flip, which flips the view like a card as you can see in the recording below:

This animation uses rotateY to rotate the view around its Y-axis and translateX to move the right side of the view to the left. The result of this is that the view is not just rotated around its centre but that the right side of the view is "flipped out" which is a nicer animation.

For this animation, we need to rotate the new view and place it on the back of the old view so they become like two sides of a card. For this, we add transform: rotateY(180deg) to the new view. This will put both views back to back like two sides of a card.

Usually, you will apply separate animations to the old and new view but in this case that won't work. We have to rotate this card so both views are animated at the same time and this is where ::view-transition-image-pair(root) comes in. Since this pseudo-element is the parent of both ::view-transition-old(root) and ::view-transition-new(root) we have to animate this element.

Let's define a flip animation for it:

@keyframes flip {
  from {
    transform: translateX(0) rotateY(0deg);
  }
  to {
    transform: translateX(-100%) rotateY(-180deg);
  }
}

::view-transition-image-pair(root) {
  transform-origin: center right;
  animation-name: flip;
}

To make sure the view doesn't just rotate around its centre but is flipped out from the right to the left, we have to set transform-origin: center right on it.

But if we flip the view now, it will be rotated but there will be no 3D effect and the view will stay "flat". It will look like its width is transitioned to zero and then the card is turned around and its width is transitioned to the actual size:

To apply a real 3D effect to an element with CSS, we have to set perspective on its parent element. This will make the element animate in 3D. The lower the value of perspective, the more dramatic the effect will be.

This is where the pseudo-element ::view-transition-group(root) comes in. This is the direct parent of ::view-transition-image-pair(root) which is rotated so we need to apply perspective here:

::view-transition-group(root) {
  perspective: 1000px;
}

And now we have a nice 3D animation!

There is one issue we need to solve though. By default, the browser will apply a cross-fade animation to ::view-transition-old(root) and ::view-transition-new(root). If we apply another animation to these, we will override that cross-fade but since the animation is now applied to ::view-transition-image-pair(root) this cross-fade will still be applied and the flip animation will not look good as you can see here:

So what we need to do is overwrite this animation with one of our own to get rid of this cross-fade. I tried setting an animation that keeps opacity: 1 for both views but that didn't work and after some experimentation I found that setting two separate animations for the old and new view worked with a turning point at 26%.

This is where both views are rotated to 90° which is where the old view is hidden and the new view becomes visible:

/* animation for front view */
@keyframes opacity-front {
  0% {
    opacity: 1;
  }
  25% {
    opacity: 1
  }
  26% {
    opacity: 0;
  }
}

/* animation for back view */
@keyframes opacity-back {
  0% {
    opacity: 0;
  }
  25% {
    opacity: 0
  }
  26% {
    opacity: 1;
  }
}

::view-transition-old(root) {
  animation-name: opacity-front;
}

::view-transition-new(root) {
  animation-name: opacity-back;
  transform: rotateY(180deg);
}

Now the animation is a perfect rotation:

When the back button is clicked we need to flip the view back. To make this work, the class back-transition is added to <html> so we can define a custom animation to correctly flip the view back. For this, we need to define a flip-reverse animation and we also need to set transform-origin: center left on ::view-transition-image-pair(root) to flip it back from the left to the right:

@keyframes flip-reverse {
  from {
    transform: translateX(0) rotateY(0deg);
  }
  to {
    transform: translateX(100%) rotateY(180deg);
  }
}

.back-transition::view-transition-image-pair(root) {
  transform-origin: center left;
  animation-name: flip-reverse;
}
🔗
Got an interesting link for Modern Web Weekly?
Send me a DM on Twitter to let me know!