CSS & layout

Why setting perspective on :root won't flip your view transition

Why setting perspective on :root won't flip your view transition

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)

Related
CSS & layout

Naming many view transitions: attr() or match-element?

Two modern ways to give every card in a grid its own view-transition-name — attr() reading a data attribute, or the match-element keyword. They look interchangeable, then diverge the moment a navigation crosses documents or a pseudo-selector needs to address a specific snapshot.

June 24, 2026
CSS & layout

Scroll-triggered animations land in CSS: `timeline-trigger` is not `animation-timeline`

Chrome 146 is the first browser to ship CSS scroll-triggered animations. The new `timeline-trigger` property fires a fixed-duration animation when a scroll threshold is crossed, and it is a close cousin of scroll-driven animations on a very different mechanism.

June 23, 2026
CSS & layout

Grid Lanes gets a Field Guide — and a fourth layout primitive to argue about

WebKit has published gridlanes.webkit.org, a Field Guide for the new display: grid-lanes layout primitive, with a playground, a cheat sheet and six demos pitting Grid Lanes against Flexbox, Multicolumn and Grid.

June 22, 2026