Taps can be unreliable inside this app's built-in browser. For the full experience, use its menu to open this page in your browser.
Skip to content

The CSS Class That Didn't Exist

A devlog by Neeraj Kotwani, who builds every game on Elipar.

I was adding the ability to play a game directly on the homepage instead of clicking through to its page. Two places now needed to know the shape of a game frame: the real player, and the placeholder that holds its space before you tap.

Two copies of the same thing will drift. So I pulled the shape into one small shared file and had both read from it. Textbook.

It broke the frame on 37 games.

Tailwind reads your source as text

The thing I'd forgotten is how Tailwind works. It doesn't generate every possible class. It scans your files looking for class names it recognises and generates only those. Which is why the CSS is small, and it's a genuinely good trade.

It scans the files you tell it to. The config listed the app and component folders. My new shared file was in neither.

So aspect-[3/4] now appeared nowhere Tailwind was looking, and the rule was never written. The class attribute was still on the element, perfectly spelled, referring to nothing. The browser computed the aspect ratio as auto, and every portrait game's frame collapsed from 465 pixels tall to about 150.

On the homepage and on all 44 game pages. 37 of them are portrait.

Nothing complained

This is the part that stuck with me. Consider everything that had an opportunity to notice and didn't.

The build succeeded, because a class name that generates no CSS is not an error to a bundler. Nothing in the browser console, because a class with no matching rule is completely legal. No test failed, because my mobile-fit checks asserted that the frame fitted inside the viewport without overflowing, and a frame that's far too short passes that beautifully.

And the screenshots looked fine. That's the one that got me. Most of these games are dark, and a dark rectangle looks like a dark rectangle at any height. I had pictures of the bug and looked straight past them.

I found it by measuring the wrong thing on purpose. The computedflex-shrink on an element said 1 when the stylesheet in front of me plainly said otherwise, which makes no sense, and chasing that contradiction led me to listing the stylesheets the page had actually loaded and discovering the rule simply wasn't in there.

When a computed style contradicts the file you're reading, stop reasoning about specificity and check whether the rule exists at all.

The version of this that bites everyone

There's a much more common form of the same problem, and I'd internalised the rule for it years ago without ever connecting it to what happened here.

You cannot build a Tailwind class name at runtime. Write `aspect-[${ratio}]` and it will never work, because when the build ran there was no such string in your source for it to find. Everybody learns this once, usually while building a component that takes a colour as a prop.

What I hadn't joined up is that it's the same rule. The class has to be visible, as literal text, in a file the scanner reads. Runtime string building fails the "literal text" half. Moving a file fails the "a file the scanner reads" half. Same cause, and I'd only ever been taught one of its two faces.

If you want the mental model in one sentence: a Tailwind class isn't a reference to something that exists, it's a request for something to be created, and the request has to be legible to a text scanner at build time.

Two lines and a comment

The fix is adding the folder to the list of places Tailwind scans, with a comment saying it's load-bearing so nobody tidies it away again.

The tests are the more useful part. There's now an assertion that the three aspect-ratio rules are actually present in the compiled CSS, which catches this class of problem directly rather than by consequence. And the mobile-fit check asserts the frame's real measured ratio instead of only that it fits, so a frame that's the wrong shape fails even when it's comfortably inside the screen.

The general shape, and I think this generalises well past Tailwind: if a tool works by scanning your source for strings, then moving a string from one file to another can change what that tool produces, even though nothing about your code's meaning changed. A refactor that's provably safe at the language level is not necessarily safe at the build-tool level. Anything with a content allowlist, a glob, or a "safelist" is in this category.

Also: assert on the thing, not on a consequence of the thing. "It fits on screen" was true the whole time.

You can see the working version on the homepage, where the Game of the Day now plays in place at the right shape, or on any of the 47 games individually.

We use cookies to keep games running and, once ads are live, to show relevant content. See our Privacy Policy for details.