· 6 min read

The CSS that only breaks on someone else's machine

Twice this year a layout on this site worked perfectly for me and was broken for everyone else, both times for the same reason. A note on clever values, and why the failure is always invisible from where you are standing.

Twice this year I have shipped a layout that was correct on my machine and wrong on the one that mattered. Both times the CSS was valid. Both times nothing errored, nothing linted, and no build warned me. And both times the cause was the same shape of mistake, which is why I am writing it down rather than filing it under bad luck.

The shape is this: I expressed something structural as a clever value instead of as a plain rule. A value has to survive every tool between the editor and the eye, and a value that any one of them does not understand simply disappears. A rule mostly cannot be misunderstood, because there is nothing shorter to fold it into.

The first one: a hint the minifier threw away

A strip of client logos on the home page scrolls continuously. On an iPhone, after the page had been open a while, it would blink or jump back to the start at random. Nothing in the code could restart it: the markup is static and no component re-renders.

The cause was compositing. A browser promotes a continuously animating transform onto its own layer, and takes that promotion back whenever it feels like it. Under memory pressure iOS discards the layer's backing store, and the re-raster shows as a blank frame or a frame drawn at a stale offset. Nothing in the stylesheet had ever said the layer was worth keeping.

The obvious fix is the one everybody reaches for: write the animation with translate3d and a zero z, because a 3d transform asks for a layer. I did that. It worked locally. It did nothing in production, and it took reading the actual shipped stylesheet to see why: the CSS minifier folds a 3d transform with no depth back to the 2d form. What I wrote was translate3d(-50%, 0, 0). What arrived was translate(-50%), which is the same movement carrying none of the meaning.

The fix that held was will-change: transform. Not because it is better practice, and it is not: a permanent will-change keeps a layer alive whether or not it is needed, and this codebase argues against it in two other places for exactly that reason. It held because it is a declaration with no shorter equivalent, so it ships as written.

The second one: a value the browser would not compute

Months later I was building diagrams for these articles. They are real interface rather than drawings of one, laid out at real component sizes on a fixed canvas, and the canvas was scaled into whatever width the article column happened to be. To scale it I needed one number: the frame width divided by the canvas width.

CSS does not divide two lengths. There is exactly one way to get a ratio out of it, and it is a trick:

transform: scale(tan(atan2(100cqw, 1280px)));

atan2 of two lengths gives an angle, tan of that angle gives back their ratio as a plain number. It is legal, it is supported, and it worked in every browser I checked.

Then the person the site belongs to said the figures looked very zoomed in. I tuned the canvas, checked again, and shipped. He said they still were not right, and added, generously, that it might be him.

It was not him. Reason it through from the failure rather than from the code and it is obvious. That expression has to resolve a container-query unit inside a math function that must produce a bare number, which is a genuinely awkward corner of the spec. An engine that will not do it does not warn: the whole transform is invalid at parse time and is dropped. The canvas then renders at its full width inside a frame that clips it, and what you are looking at is the top-left corner of a 1280 pixel picture through a 640 pixel window.

That is not a subtle degradation. It is an enormous crop. And note what my first fix did: I made the canvas bigger to fix the zoom, which on his machine made the crop worse. I was tuning the wrong end of a rendering that did not exist for him.

The same grid twice. On the left, the rule that sizes it arrived. On the right it did not, so the grid renders at its natural size in a box too small for it. On the machine where the rule works, both panels look like the left one, which is why nobody who can see it can find it.

What the two have in common

In both cases the browser did what it was told. In both cases the tool in the middle, a minifier once and a parser once, quietly removed something that was carrying meaning. And in both cases the person who could see the bug and the person who could fix it were different people, which is the part worth planning for.

Three things I do differently now.

Read what shipped, not what you wrote. The marquee fix took an hour longer than it needed to because I was reading my own stylesheet. One curl for the built CSS chunk and a grep for the declaration would have answered it in a second. If a fix depends on a specific declaration reaching the browser, go and look at it in the browser's own copy.

Prefer a named property to a computed value. will-change: transform over a transform that happens to imply a layer. A grid that reshapes over a canvas that scales. This is not a rule about performance, it is a rule about how many things can silently disagree with you between here and there. Cleverness in a value has a long supply chain; cleverness in a rule does not.

When someone says it looks wrong and you cannot reproduce it, believe the person, not the screenshot. Two rounds of tuning went into a mechanism that was never running for him. The question "what would have to be true for this to look like that" would have got there immediately, because "the scaling declaration was dropped" predicts exactly the symptom he described, including the detail that my fix made it worse.

The unglamorous ending

Both fixes are duller than what they replaced. The marquee holds a compositor layer with an ordinary declaration and a comment explaining why. The diagrams do not scale at all any more: they lay themselves out at whatever width they are given, with the components at their real sizes, and the grid of twelve tiles becomes three columns on a narrow card instead of shrinking into an unreadable stamp. That last part is a straight improvement I would not have made if the clever version had worked, which is the small consolation available here.

There is a version of this note that ends with "avoid clever CSS", and I do not believe that. Cleverness is how anything interesting gets built, and the tan(atan2()) trick is genuinely good. The lesson is narrower and more useful: know which of your clever bits are load-bearing, and make sure those are the boring kind. A layout that depends on an exotic value has a failure mode you cannot see from your own desk, and the first person to find it will be someone who cannot tell you why.