Why setting perspective on :root won't flip your view transition
Iris Calderón
You want a card-flip view transition. Cards rotate out in 3D on the old page, fresh ones rotate in on the new one. You opt into the transition, write your keyframes, set perspective: 1100px on :root the way every 3D tutorial taught you, and the animation runs perfectly... flat. No errors. Nothing red in devtools. Just a slide where a flip should be.
Sunkanmi Fafowora at CSS-Tricks walks through exactly this trap and where the perspective actually has to live. The diagnosis is shorter than the cure deserves: the property version of perspective has nothing to attach to inside a view transition.
The pseudo tree, briefly
A view transition snapshots the page, then renders a parallel tree under html:
::view-transition
::view-transition-group(name)
::view-transition-image-pair(name)
::view-transition-old(name)
::view-transition-new(name)
Every snapshot you address — the whole-page root snapshot, or a named one — sits inside an image-pair, which sits inside a group, which sits inside the single ::view-transition root. That tree is what you're animating against, not the original DOM elements.
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.
Source: CSS-Tricks (css-tricks.com)