For a cross-document run, you opt in on both pages:
@view-transition {
navigation: auto;
}
Then you reach for ::view-transition-old(root) and ::view-transition-new(root) to attach keyframes to the outgoing and incoming snapshots. That part works. The 3D doesn't.
Why the property quietly fails
perspective is a parent-applied CSS property. You set it on the element that contains the transformed children, and it establishes a 3D rendering context for everything inside. That is the whole shape of how it works in the cascade.
The view-transition tree breaks that shape. The snapshot pseudo-elements are children of ::view-transition-image-pair, which is a child of ::view-transition-group, which is a child of ::view-transition. Walk up that chain looking for a real parent and you do not find one. You find pseudo-elements all the way down. Set perspective on html, :root, body, ::view-transition-group(root), or ::view-transition itself, and none of those carry into the snapshot's rendering context. The animation falls back to a 2D projection.
This is the same gap that bites you the first time you reach for an inherited property here. The view-transition pseudo tree is not the DOM. You can target nodes inside it, but you cannot lean on the cascade behaving as if there is an ordinary parent above them.
Switch to the function form
The fix is to stop applying perspective from the outside and put it on the transform itself:
@keyframes flip-out {
0% { transform: perspective(1100px) rotateY(0deg); opacity: 1; }
100% { transform: perspective(1100px) rotateY(-90deg); opacity: 0; }
}
@keyframes flip-in {
0% { transform: perspective(1100px) rotateY(90deg); opacity: 0; }
100% { transform: perspective(1100px) rotateY(0deg); opacity: 1; }
}
::view-transition-old(root) {
animation: flip-out 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
transform-origin: center center;
}
::view-transition-new(root) {
animation: flip-in 0.3s cubic-bezier(0, 0, 0.6, 1) 0.3s backwards;
transform-origin: center center;
}
perspective() the function is part of the transform property. It lives on the element being transformed, not on a parent. That is the bit the snapshot tree can honour: every keyframe is applied directly to ::view-transition-old(root) or ::view-transition-new(root), and the perspective rides along with the rotation.
Same value, same intent, completely different attachment point. The property declares a 3D context for whatever it contains. The function declares a 3D context the element carries with it. Inside the view-transition tree, only the second sentence has a referent.
Worth knowing before you ship
The CSS-Tricks piece flags an extra WebKit flattening issue with 3D contexts in view transitions, referenced as WebKit bugs #283568 and #302166. If your flip runs cleanly in one engine and goes flat in another, you have not necessarily mis-cascaded; the engine may not yet honour 3D context inside view transitions. Worth a real cross-engine pass before you wire a flip into production navigation.
For your next transition: write the keyframes with perspective() baked into the transform from the start. Do not reach for the property form on :root as a fallback, not as a "just in case". Inside the snapshot tree, it is not a fallback. It is a no-op.