Skip to content

Writing

Four CSS traps, and the one property family behind all of them

A mask that cancelled a 3D transform, a perspective on the wrong element, a header that ate its own menu, and a timeline measured against the wrong box. All four looked right in the state anyone would check first.

4 min read

Four bugs, found weeks apart, that turned out to be the same bug wearing different clothes. Each one rendered correctly in the state anybody would check first, and each one was invisible until something specific happened.

They are all grouping and containing-block properties. Once you know that, they stop being surprises and become a checklist.

One: a mask cancelled preserve-3d

The hero carries a globe built from CSS 3D transforms — a few hundred dots positioned on a sphere, turned by the compositor. It looked perfect at rest and collapsed to a flat sliver a quarter of the way through every rotation.

The mask that faded its edge was on the same element as transform-style: preserve-3d.

The fix is structural, not a value change: the mask needs its own element, and the 3D context needs its own.

Two: perspective applies to children, not to itself

Same drawing, next problem. perspective was on the element that also carried the rotation, and it did nothing.

Before

Perspective and rotation on one element. Renders. Has no depth, and the reason is not visible anywhere.

After

Perspective on the parent, rotation on the child inside it. Identical markup weight, correct result.

Three: a header that ate its own mobile menu

This one came in with screenshots, and it was the best bug of the four.

At the top of the page the mobile menu opened correctly. Scrolled down, the same button produced a transparent strip with the page showing through and no links in it.

The header gets a backdrop-blur once you scroll. backdrop-filter, filter and transform all make an element the containing block for its position: fixed descendants. The overlay lives inside the header, so inset: 0 resolved against a 60px bar instead of the viewport.

The surface is now a layer inside the header and the header itself carries no filter. Two tests hold it: one opens the menu at scroll 2600 and asserts the overlay fills the viewport, and one asserts the header's own transform, filter and backdrop-filter all compute to none.

Four: a timeline measured against a box that never moves

The newest one. A scroll-driven stagger inside a horizontally scrolling table froze — the first rows landed past their range and the last sat permanently mid-fade, looking like a row that had failed to load.

overflow-x: auto makes overflow-y compute to auto as well. So that container is a scroll container on both axes, and a view timeline is measured against its nearest scrollport ancestor — which never scrolls vertically. Progress was frozen at a single value forever.

Why the timeline moved rather than the overflow

Setting overflow: clip would fix the measurement and stop the table scrolling, which is the one thing that container exists to do. The timeline moved to a wrapper outside it instead. Named view timelines resolve up the ancestor chain by tree scope, not by scroll container, so the rows still find it by name.

The checklist this produced

All four failed silently. None threw, none warned, and all four rendered a page that looked fine in the first state anyone checks. So the rule is not "be careful", it is a question asked before a property goes on:

PropertyWhat it silently does
mask, filter, opacityGroups — flattens any preserve-3d on the same element
overflow (non-visible)Groups, and makes the element a scrollport for view timelines inside it
transform, backdrop-filterBecomes the containing block for position: fixed descendants
perspectiveApplies to children, never to the element itself
Before adding one of these, check what is positioned or 3D-transformed inside the element.

Grouping and containing-block properties are this codebase's recurring foot-gun. They are now the first thing checked, and the checklist is in the contributor file rather than in anybody's memory.

Start here

Tell us what you are building

Or what is breaking, or what has to go faster. You will get a straight answer from an engineer who would do the work.