Shop

Spend your points on upgrades

Cat Cosmetics
← Back to Blog

The 39 Pixels That Shouldn't Exist

It was close to midnight and I was doing the thing every developer does right before bed: scrolling through their own app, half looking for bugs, half just admiring the thing they built. I found a bug. One of the cards in my compound library had what looked like an entire extra card’s worth of empty space sitting underneath it, for absolutely no reason I could see.

I closed the app, told myself I’d look at it in the morning, and then obviously did not do that. I stayed up chasing it instead, and it took most of the next day too. As far as I can tell, this exact bug isn’t written down anywhere: not in a GitHub issue, not in a WebKit bug report, not in some forgotten Stack Overflow answer from 2019. I only found the actual cause because I happened to own an iPhone and a Mac to plug it into, which turned out to be the whole trick.

This is that story, mostly so that if you ever run into the same thing, you can skip straight past the five things I tried that didn’t work.

The setup

Telos has a compound library page: a grid of about 140 cards, one per compound, each with an icon, a name, a category, a little dose pill, and a two-line description clipped with Tailwind’s line-clamp-2. There’s nothing exotic going on here. It’s a plain CSS grid, single column on mobile, gap: 0.5rem between rows, the kind of layout you build without thinking twice about it.

Except some of the cards had this gap underneath them that didn’t belong. Not all of them, just some, scattered through the list with no obvious pattern. Scroll past three completely normal cards, hit one with a gap the size of a missing card, then back to normal like nothing happened.

what I expected what Safari showed me empty, unexplained
Same markup, same CSS, two different browsers. Chrome rendered the left side, every time. My phone rendered the right side, every time, on the same three or four cards, no matter how many times I reloaded.

I reloaded a dozen times just to be sure. Same cards, same size gaps, every single time, which was almost more frustrating than if it had been random. Then I opened the same page in Chrome, forced it into the exact same single-column width, and got nothing. Perfectly even spacing, top to bottom, not one gap anywhere.

Safari-only and completely consistent. That combination should have made it easy. It did not.

Everything I tried that didn’t work

Here’s the shape of the next several hours, before I get into it properly:

1 2 3 4 5 6 the fix
1: raise the clamp height. 2: move the entrance animation off the grid item. 3: remove the animation entirely. 4: replace line-clamp outright, orange everywhere except this one, which actually mattered, just not enough by itself. 5: try CSS containment. 6: stop letting Safari measure the row at all. That's the one that worked.

First guess: it’s the long descriptions. The worst gaps sat under cards with noticeably longer text than their neighbors, and that felt like enough of a pattern to chase. I’d already capped the description at min-h-10, a height floor, to keep card heights consistent across a row, so my theory was that a floor still forces the browser to measure the real content to check whether it’s taller, and that measurement was somehow leaking out into the gap below. I swapped it for a hard h-10 instead, shipped it, waited for the deploy, reloaded on my phone with actual hope that I was done.

Same gaps. Same cards. Pixel for pixel, nothing had changed.

Second guess: it’s the entrance animation. Every card faded in with a small framer-motion transform on mount, and I’d read that WebKit has a rough history with CSS grid items that carry a transform, specifically that a transformed grid item can throw off how a row’s height gets calculated, even though transform isn’t supposed to touch layout at all. So I moved the animation one level deeper, off the actual grid item and onto a plain wrapper’s child, which felt like a genuinely clever fix at the time.

Still broken. Exact same cards, exact same size.

Third guess, and I was getting a little annoyed by now: fine, kill the animation entirely. If moving it one level in wasn’t enough, maybe no version of it was safe, so I ripped framer-motion out of the shared list component completely. Zero transforms, anywhere, in that whole grid.

Still broken. Three separate, individually reasonable theories, each one shipped and verified live, and the bug hadn’t moved an inch. That’s usually the point where you realize you’ve been debugging the wrong layer of the problem entirely, and I was pretty sure that’s where I was, I just didn’t know which layer yet.

Actually getting real data

I was guessing at what Safari was doing instead of just asking it, which in hindsight is an embarrassing amount of time to spend not asking the one machine that actually knew the answer. So I plugged my phone into my Mac, opened Safari’s remote inspector (Develop menu, pick the device, pick the tab), and ran a one-line script in the Console against the live page:

const grid = [...document.querySelectorAll('div')]
  .find(d => getComputedStyle(d).display === 'grid');
[...grid.children].map(el => ({
  name: el.querySelector('h3')?.textContent,
  height: Math.round(el.getBoundingClientRect().height)
}));

This is what I should have done an hour in, not three fixes deep. It handed me real numbers straight from the device, instead of another screenshot I was squinting at on my laptop trying to eyeball pixel counts.

The gap between every single card, it turned out, was a clean uniform 8px, everywhere, with no exceptions. The row spacing had never actually been the problem. What varied was each card’s own height: 123px here, 124px there, then a scattering of cards sitting at 143, 162, 181, even 200. And the size of each jump lined up almost exactly with one extra line of text at that font size, which tracked suspiciously well with how long each card’s description happened to be.

Which sent me right back to line-clamp-2. The thing I’d already “fixed” in guess number one.

What was actually going on

Turns out there were two separate bugs stacked on top of each other, and I’d only been fixing at the seam between them.

Bug one: Tailwind’s line-clamp-2 compiles down to an old WebKit hack, display: -webkit-box paired with -webkit-line-clamp. On my phone’s build of Safari, combining that with an explicit height didn’t reliably cap anything at all. Longer text just kept growing the box, proportional to how much text there was, height property or not. So I ripped line-clamp out everywhere in the app and replaced it with a plain fixed height plus overflow: hidden. You lose the ellipsis at the cut-off point, but it’s unambiguous: every browser respects a fixed height and an overflow rule, no legacy box model involved anywhere.

I measured the actual <button> for the card right after shipping that change, and it came back at 123px. Exactly right. For about ten minutes I thought I was finally done.

Then I checked the grid item wrapping it, and it was still 162px.

Bug two, and this is the one that actually explains the number in the title: even with the description correctly clipped to 40px on screen, its natural, un-clipped content still wanted 95px of space, which scrollHeight was happy to confirm. Whatever Safari’s grid engine uses to decide how tall an auto row should be seemed to be looking at that unclipped 95px figure, not the 40px that was actually painted on screen. The card itself rendered exactly right. The invisible box the grid built around it didn’t match, and the leftover space just sat there quietly below the card, looking exactly like a layout bug even though every single element inside it was behaving perfectly.

I had one more idea before I let myself accept how strange this was. CSS containment, contain: layout paint, on the grid item, which is supposed to stop a descendant’s content from being able to influence how an ancestor gets measured. It’s a reasonable technique, and one the app already used elsewhere for exactly this kind of isolation. I shipped it, confirmed with a fresh, timestamped console check that I wasn’t just staring at a stale cached build for the fifth time, and it changed nothing.

grid item, Safari measured this at 162px the <button> card, actually renders at 123px icon, name, category, dose pill description, clipped to 40px on screen ✓ ≈39px
The card and the text inside it both render at the right size. The extra space sits between the button's real bottom edge and wherever Safari decided the row needed to end, and it appears to be sizing that boundary off the unclipped content, not the clipped one.

The actual fix

By this point every card’s real height was fully deterministic. Fixed-size icon, single-line truncated name and category, and now a properly hard-clipped description. There was no legitimate reason left for the row height to be auto at all, since nothing inside the card could actually vary anymore.

So instead of trying, yet again, to convince Safari to measure things correctly, I just took the measurement away from it entirely:

- grid gap-2 grid-cols-1 md:grid-cols-2 xl:grid-cols-3
+ grid gap-2 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 auto-rows-[128px]

Pin the row track to a fixed pixel value and there’s no auto sizing pass left for the bug to hide inside. I reloaded, ran the same console check one more time, and this time the grid item and the button finally matched: 123 and 123.

baseline 124 143 162 181 200
Real numbers straight off getBoundingClientRect(), run on the actual device. Each step is roughly one line of text, the clue that pointed back at line-clamp, and then past it to the grid itself.

What I took away from this

A browser that just quietly refuses to reproduce your bug isn’t a coincidence you get to wave off. It’s information. I burned three separate fixes assuming that Chrome working meant my code had to be fine, and the problem had to be somewhere I just hadn’t looked yet on the Safari side. That part was true. I just kept guessing at the wrong place to look.

There’s a second lesson buried in there too: measure the thing the bug is actually in, not the thing that’s easiest to check. I checked the card’s visible content three separate times, and it was correct every single time, which is exactly why it took so long. The real bug was one level up, sitting inside a box with no border, no background, and nothing rendered inside it to give away that it even existed.

And, more narrowly, -webkit-line-clamp is apparently just not safe to pair with an explicit height on some builds of Safari. If you’re clamping text inside anything remotely complex, a plain fixed height with overflow: hidden is more boring to look at, but it will not quietly lie to you six months later at midnight.

Anyway. The grid is fixed now, the app looks the same on every device I own, and I have a genuinely new appreciation for Safari’s remote inspector, which I’d never once opened before this week and will absolutely be reaching for again.