3D Transforms, perspective and backface-visibility Costs
Part of Hardware-Accelerated Properties in Core CSS Animation Fundamentals.
The problem
A pricing page flips cards over on hover to show details. A gallery tilts images towards the pointer in 3D. A product viewer rotates a CSS cube. All three look smooth on a desktop and stutter on a phone, and the Layers panel shows far more layers than elements. Meanwhile, an old stylesheet sprinkles transform: translateZ(0) on dozens of elements “for GPU acceleration”.
3D transforms are compositor-friendly, but they are not free. They change how elements are grouped into layers, how those layers are sorted, and how much texture memory the page holds.
Root cause analysis: 3D decides layering
Any 3D transform promotes. A non-identity 3D transform — translateZ(), rotateY(), perspective() in the transform list, translate3d() — gives the element a compositing reason. That is why translateZ(0) was used as a promotion hack: it is an identity transform that still counts as 3D. Every hacked element costs a texture sized to its area times the square of the device pixel ratio, whether it animates or not, the problem described in layer promotion strategy.
transform-style: preserve-3d creates a shared 3D context. Children keep their own 3D positions instead of being flattened into the parent’s plane. The compositor must then keep each child as a separate layer, sort those layers by depth each frame, and — if planes intersect — split them to draw correctly. A cube made of six faces in preserve-3d is six layers plus sorting; a card with front and back faces is two layers plus sorting.
perspective on the parent versus perspective() in the transform. The perspective property on a container sets one vanishing point shared by all children, and is cheap to reason about. A perspective() function inside each child’s transform gives each child its own vanishing point, which looks different and makes each transform list longer and harder to interpolate.
backface-visibility: hidden hides an element when its back faces the viewer. It is essential for flip cards — without it the front face’s mirror image shows through — and it is itself another compositing reason in some engines.
Step-by-step resolution
Production code pattern
/* Flip card: two faces genuinely need a shared 3D space. */
.flip {
perspective: 900px; /* one vanishing point for the card */
}
.flip__card {
display: grid;
transform-style: preserve-3d;
transition: rotate 520ms cubic-bezier(0.3, 0.7, 0.2, 1);
}
.flip__face {
grid-area: 1 / 1; /* stack both faces in the same cell */
backface-visibility: hidden;
}
.flip__face--back { rotate: y 180deg; }
.flip:is(:hover, :focus-within) .flip__card { rotate: y 180deg; }
/* Tilt: no preserve-3d needed, one element, one layer while moving. */
.tilt {
perspective: 700px;
}
.tilt__image {
transition: rotate 200ms ease-out;
rotate: var(--tilt-axis, 0 1 0) var(--tilt, 0deg);
}
@media (prefers-reduced-motion: reduce) {
.flip__card { transition: none; transform-style: flat; }
.flip__face { backface-visibility: visible; transition: opacity 150ms linear; }
.flip__face--back { rotate: none; opacity: 0; }
.flip:is(:hover, :focus-within) .flip__card { rotate: none; }
.flip:is(:hover, :focus-within) .flip__face--back { opacity: 1; }
.flip:is(:hover, :focus-within) .flip__face--front { opacity: 0; }
.tilt__image { rotate: none; transition: none; }
}
Rendering Impact: composite, with extra layers. The flip rotates a 3D context on the compositor, which sorts two face layers per frame; the tilt is a single promoted layer. Neither touches layout or paint while animating.
The reduced-motion branch is more than transition: none. Without rotation, both faces would sit on top of each other with the back face hidden only by its rotation, so the branch flattens the card and swaps faces with opacity instead.
Verification checklist
Constraints and trade-offs
preserve-3dis flattened byoverflowother than visible,filter,opacitybelow 1 and several other properties on the same element.- Depth sorting of intersecting planes is expensive and can produce z-fighting artefacts.
backface-visibility: hiddenmay itself cause promotion; apply it only to faces that flip.- Text on rotated faces is rasterised into textures and can look soft mid-rotation.
- Hover-only flips hide content from touch and keyboard users unless focus and tap states are also handled.
Frequently asked questions
Does translateZ(0) make animations faster?
It forces a layer, which can remove a paint cost for an element that animates transform or opacity, but will-change does that more explicitly and can be removed afterwards. On elements that do not animate, it only costs memory.
When do I need transform-style: preserve-3d?
Only when children must exist in a shared 3D space, such as the two faces of a flip card or the sides of a cube. A single element tilting in 3D does not need it.
Why does my flip card show a mirrored front face?
The faces are missing backface-visibility: hidden, or preserve-3d is being flattened by overflow, opacity or filter on the card element.
Related
- Hardware-Accelerated Properties — the parent topic
- Stacking Contexts and Accidental Layer Explosions — how extra layers multiply
- Fixing Blurry Text After Transform Animations — rasterised text on rotated layers