In case you have performed round with view transition a bunch, you will have observed that 3D transitions between two pages (i.e., cross-document view transitions) don’t appear to work. That’s, not less than not with out the browsers flattening issues first.
Picture components are the most effective instance to show this as a result of, just like the snapshots a browser takes of the before-after states in a view transition, photos are changed components so, in concept, we should always have the ability to use them as a kind of lowered take a look at case for 3D animations. For instance, flipping one picture to disclose one other on click on seems like this:
It’s necessary to notice that, for the animation to work correctly, we have to set the perspective property on the picture’s mum or dad container (in our case, it’s the .scene ingredient). In any other case, the 3D transformation is merely flat. It kind of angles the ingredient’s look:
In CSS, the mum or dad’s persepective is utilized to all its kids, excluding itself:
.scene {
perspective: 1200px;
.card { /* will get perspective */ }
}
What’s necessary right here is the HTML construction. Particularly how the .scene container sits on high of the kid .card components, making the 3D impact come to life so the flip seems the way it ought to:
Maybe our keyframe animation to flip the .playing cards is one thing like this:
@keyframes flipOut {
from {
remodel: rotateY(0deg);
}
to {
remodel: rotateY(-90deg);
}
}
Which we apply to the .playing cards like this:
.card.flip-out {
animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.card.flip-in {
animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards reverse;
}
…the place the animates runs forwards when the .flip-out class is appended to the .card (courtesy of JavaScript awaiting a click on) and runs in reverse when the .flip-in class is appended.
That’s the setup for the way a cross-document view transition should work, too, proper? If a picture helps a 3D animation, then a view transitions snapshot ought to do the identical. Let’s poke at that.
Establishing the view transition
First issues first, we now have to choose into view transitions on each pages with the @view-transition at-rule by setting the navigation descriptor to auto:
@view-transition {
navigation: auto;
}
If we had been to do nothing else, then one web page fades into one other when navigating between the 2. It’s essentially the most primary of all cross-document view transitions.
How will we customise issues? We use the ::view-transition-old() and ::view-transition-new() pseudo-classes, the place the previous is the “previous” snapshot and the latter is the “new” one. Just like the .card components we used within the final instance, that’s the place we set the keyframe animation:
::view-transition-old(root) {
/* animation goes right here */
}
::view-transition-new(root) {
/* animation goes right here */
}
The root parameter tells the view transition to focus on the entire web page and all the weather created (and never created) by the view transition’s default snapshot group.
Right here’s the issue
Let’s say we wish to apply that very same 3D flip to the complete webpage, the place the snapshot of the “previous” web page flips into the “new” web page. Once more, a 3D animation asks us for 2 issues:
- The
perspectiveproperty on the mum or dad ingredient so its kids get that 3D impact - An animation on the web page for when the view transition occurs
However: What precisely will we set the attitude on, as in, what's the mum or dad ingredient right here?
Since view transitions take snapshots of the complete webpage, we would assume (logically) it will be the ingredient (or the :root), proper? I imply, the DOM tree seems like this when a view transition is current:
html
├─ ::view-transition
│ ├─ ::view-transition-group(card)
│ │ └─ ::view-transition-image-pair(card)
│ │ ├─ ::view-transition-old(card)
│ │ └─ ::view-transition-new(card)
│ └─ ::view-transition-group(identify)
│ └─ ::view-transition-image-pair(identify)
│ ├─ ::view-transition-old(identify)
│ └─ ::view-transition-new(identify)
├─ head
└─ physique
└─ …
So, the complete snapshot needs to be the place we put the perspective. Proper? Seems, no.
In actual fact, does nothing in any respect! You’re left with this as an alternative of the attractive 3D flip we had been ready to make use of on the playing cards earlier:
Right here’s the code I used to be working with:
/* Cross-document View Transition opt-in */
@view-transition {
navigation: auto;
}
/* 3D flip: Previous web page flips away, new web page flips in */
@keyframes flip-out {
0% {
remodel: rotateY(0deg);
opacity: 1;
}
100% {
remodel: rotateY(-90deg);
opacity: 0;
}
}
@keyframes flip-in {
0% {
remodel: rotateY(90deg);
opacity: 0;
}
100% {
remodel: rotateY(0deg);
opacity: 1;
}
}
::view-transition-old(root) {
animation: flip-out 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
transform-origin: heart heart;
}
::view-transition-new(root) {
animation: flip-in 0.3s cubic-bezier(0, 0, 0.6, 1) 0.3s backwards;
transform-origin: heart heart;
}
Word: I didn’t reverse the animation right here since we flip to -90deg after which from 90deg. Not precisely the identical!
And it doesn’t work, regardless of if perspective is on html or :root:
/* 👎 */
html {
perspective: 1100px;
}
/* 👎 */
:root {
perspective: 1100px;
}
I did some digging and found that perspective (and 3D transformations generally) is certainly one of a number of CSS properties that might produce an uncommon impact. (Go away it to Bramus to have the reply!)
So… What will we do? Some concepts got here to thoughts, however sadly failed:
- I attempted setting the
perspectiveproperty on thephysique. - I attempted setting
perspectiveinside::view-transition-group(root). - I attempted setting
perspectivecontained in the::view-transitionpseudo.
There’s truly an excellent easy workaround to this, and I can’t consider it took me this lengthy to determine it out — don’t use perspective in any respect!
The answer
Brief story: we now have to make use of the perspective() operate as an alternative of the perspective property. And never inside any of the ::view-transition-* pseudos as you would possibly count on, however contained in the @keyframes animation:
@keyframes flip-out {
0% {
remodel: perspective(1100px) rotateY(0deg);
opacity: 1;
}
100% {
remodel: perspective(1100px) rotateY(-90deg);
opacity: 0;
}
}
@keyframes flip-in {
0% {
remodel: perspective(1100px) rotateY(90deg);
opacity: 0;
}
100% {
remodel: perspective(1100px) rotateY(0deg);
opacity: 1;
}
}
This easy, however huge change strikes the scene from a flat meh to an exquisite ah yeah:
Right here’s why, apparently. The view transition pseudo-element tree is rendered outdoors the traditional HTML circulate. Extra particularly, the complete view transition tree is rendered above the DOM in its personal layer. Nonetheless, notably for ::view-transition, I’m not too positive why that is the case, however my greatest guess could be that every view transition group robotically has its place and remodel values overridden by the browser; therefore, interfering with the perspective.
The distinction between perspective and perspective()? The perspective property is utilized to the mum or dad ingredient, whereas perspective() is a remodel property operate utilized on to the ingredient itself. And because the view transition pseudo tree doesn't have a real mum or dad, we’ve gotta use perspective() because it doesn’t require a mum or dad. Phew.
To recap…
Setting perspective on the html, :root, or any of the view transition pseudo-class received’t work. And in case you have been struggling to seek out the answer, like I used to be, I believe this little, however huge perspective() change will clear up that situation for those who ever come throughout it. Take it from me, I battled with this for weeks until I got here again at the moment to rant about it and found an answer to it. A perk of writing!





![How creators and entrepreneurs are utilizing AI to hurry up & succeed [data]](https://blog.aimactgrow.com/wp-content/uploads/2025/06/Untitled20design-Apr-07-2023-08-24-35-4586-PM-120x86.png)


