Vibe Arcade Blog

Dev stories, game design, and the art of building with AI

How One CSS Rule Fixed 20 Games at Once

One line of CSS. One commit. Twenty games fixed. This is the story of a bug fix that took thirty seconds to write — and the architecture that made it possible.

· Vibe Arcade

CSS architecture browser games engineering

The Bug

Last Sunday, we got a report that buttons across our games felt broken on tablets. Players had to tap twice to start a game, restart after a game-over, or open settings. Some taps just didn't register at all. Others had a noticeable delay — long enough to make you wonder if the button was working.

The bug wasn't in one game. It was in all of them. Every game on Vibe Arcade uses HTML <button> elements for UI — start, pause, restart, settings. And every one of those buttons was misbehaving on touch devices.

If you've shipped for mobile, you might already know the culprit. Mobile browsers add a ~300ms delay to click events on elements that could be part of a double-tap-to-zoom gesture. The browser waits to see if a second tap is coming before firing the click. On a game UI, 300ms is an eternity.

The Fix

Here's the entire patch:

button { touch-action: manipulation; }

That's it. One CSS rule. The touch-action: manipulation declaration tells the browser: "This element supports panning and pinch-to-zoom, but not double-tap-to-zoom." With double-tap-to-zoom off the table, the browser no longer needs to wait 300ms to disambiguate a single tap from the start of a double-tap. Taps fire immediately.

We added this line to our shared stylesheet, committed it, and deployed. Every game on the site — twenty at the time — inherited the fix instantly. No per-game patches. No release coordination. No merge conflicts. One line, one commit, twenty games fixed.

The System That Made This Possible

The reason one CSS rule could fix twenty games is that all twenty share a single stylesheet. Every game loads the same main.css — layout, typography, button styling, section structure. But our games don't all look the same. The structural CSS is shared; color and theme variation is per-game. Each game overrides a small set of CSS custom properties:

body {
    --game-accent: #ff00ff;
    --game-secondary: #00ffcc;
    --game-bg: #0a0012;
    --game-surface: rgba(255, 0, 255, 0.08);
}

The shared stylesheet references these variables for colors and gradients. Layout rules — button sizing, section spacing, container structure — are global and identical across every game.

This is why button { touch-action: manipulation; } worked as a global fix. Button behavior is structural, not thematic. It belongs in the shared layer. Every game inherits it. And when a new game gets built tomorrow, it will inherit this fix automatically — the fix is part of the platform now, not a per-game patch that someone has to remember to apply.

Fix Once Globally: The Broader Principle

The CSS fix crystallized a principle we'd been learning the hard way: when a bug affects every game, the fix should live in exactly one place. We've identified three canonical locations where a single change propagates everywhere:

  1. The shared stylesheet — for layout, typography, and UI behavior. Add a rule once, every game inherits it. The touch-action fix is the clearest example.
  2. A game-integration lint script — for structural and wiring rules. A grep-based check runs against every game on every change, catching classes of bugs before they ship.
  3. The game builder's authoring rules — patterns that our build pipeline follows when creating new games. Fixes become the default, so new games are born correct.

Each layer catches a different kind of problem. CSS rules fix presentation and behavior. Lint rules catch structural wiring mistakes. Authoring rules prevent entire categories of bugs from being introduced in the first place.

The Lint Script Story

The same week as the CSS fix, we discovered something worse than a cosmetic bug. Our leaderboard widget has a method called checkScore that determines whether a player's score qualifies for the leaderboard. Several games were wired to call the widget but never actually invoked checkScore — they rendered the leaderboard UI but silently skipped the score-submission logic. Players were finishing games, seeing a leaderboard, and having their scores quietly dropped.

This had been shipping for weeks. Nobody noticed because the leaderboard still appeared — it just never updated with new scores for those games.

The fix for each affected game was straightforward: add the missing checkScore call. But the more important fix was a single grep rule added to our lint script:

# Every game that includes LeaderboardWidget must call checkScore
grep -l "LeaderboardWidget" games/*/index.html | while read f; do
    grep -q "checkScore" "$f" || echo "FAIL: $f includes LeaderboardWidget but never calls checkScore"
done

This check now runs against every game, on every change. It caught the existing broken games retroactively, and it will catch any future game that makes the same mistake. One grep rule, permanent protection against an entire class of bug.

There's a philosophy here: grep-based lint rules are orders of magnitude cheaper than sophisticated analysis passes. They're fast, obvious, and they catch the bugs that actually happen in practice. We run them first, on every change. When a pattern of bug keeps recurring, the first question is: "Can we write a grep for this?"

When Per-Game Sweeps Are Still Necessary

We'd be lying if we said everything is a one-line global fix. The same week we added touch-action: manipulation, we ran a migration across every game to move inline <style> blocks to CSS custom properties. That meant opening each game file, replacing hardcoded color values with variable references, and testing that each game still looked right. Twenty files. Real time.

But it was a one-time migration cost. The new pattern lives in the global layer now. Future games inherit it automatically. We'll never do that sweep again.

The honest version of the fix-once-globally principle: sometimes you have to do the per-game sweep first to extract the pattern, then capture it in the shared layer so nobody repeats it. This principle wasn't planned from day one. It was learned. We did the painful per-game work, noticed we kept doing it, and built the infrastructure to stop.

The test for whether a sweep was worth it: did it end with a new rule in the shared stylesheet, a new check in the lint script, or a new pattern in the authoring guide? If yes, it paid for itself. If you just fixed twenty files and didn't capture the pattern anywhere, you'll be doing the same sweep in three months.

The Takeaway

One CSS rule fixing twenty games isn't magic — it's the result of a shared stylesheet that every game inherits from. One grep rule catching a wiring bug across every game isn't clever — it's the result of deciding that structural checks should live in one place, not twenty code reviews.

The satisfying part isn't the individual fix. It's knowing that every game you build next week will be born with the fix already applied. That's the compound interest of fixing things in the right place.


Related reading: What Is Vibe Coding? · Vibe Coding Tools: From Chatbots to AI IDEs · Building a Universal Leaderboard