rows.com

When CSS Takes Over Logic, Your Stack Gets Harder to Read

Pulling logic out of JavaScript and into CSS might make a single file look cleaner, but Gabriel Shoyombo warns that it often makes the whole application harder to reason about.

4 min readArticles on Smashing Magazine — For Web Designers And Developers
When CSS Takes Over Logic, Your Stack Gets Harder to Read

The promise of CSS replacing JavaScript is seductive, fewer lines, faster reviews, a cleaner diff. But as Gabriel Shoyombo's deep dive makes clear, that trade often hides a steep cost: a codebase that becomes harder to reason about, not easier. We think this is a classic case of a tool doing exactly what it claims while quietly shifting complexity into places developers rarely check. The real question isn't whether CSS can handle logic; it's whether your team can still predict what happens when the cascade, specificity, and layers all collide at runtime. This matters in the same way that Microsoft Retires Two Developer Tools, Redirects Teams to Manual Workflows matters, when a platform removes a layer of abstraction, the hidden work doesn't vanish; it just moves somewhere else. And when Anthropic's shift to server-side source control redefines how AI teams work, the lesson is the same: the structure you choose for your code determines how much of its behavior you can actually hold in your head.

The specific trap here is specificity leaking across component boundaries. Shoyombo walks through a case where a `.card:has(img) p` rule, intended to style a hero image, accidentally catches a comment's avatar because `:has()` borrows weight from its argument. The fix, using child combinators or `:where()` to drop specificity back to zero, is straightforward once you know the problem exists. But the problem is invisible until a bug report says "spacing looks off sometimes" and you have to reconstruct which selector is winning at runtime. That invisible cost is the real danger. It's not that CSS can't handle the work; it's that the rules for who wins are buried in the cascade's order of appearance, which changes based on file load order, layer placement, and even whether a rule is nested. The same dynamic appears in the CSS-in-JS example Shoyombo cites, where refactoring JavaScript changes the order injected styles appear, breaking styles in production that worked locally. The tool isn't broken; the system is.

What makes this urgent is the performance angle. Shoyombo's benchmark tells a story that should give every team pause. A single `:has()` rule anchored to `body` costs about 1.4ms, fine on its own. But combine it with a second, equally harmless rule, and the browser restyles 5,001 elements to change one. At 20,000 rows, that becomes 31ms per toggle, nearly two frames dropped. The two rules don't conflict; they simply coexist. The browser's engine does exactly what it was told, and the result is a performance tax nobody wrote. This isn't a bug. It's a property of the system that only reveals itself under load.

The concrete takeaway: before you replace JavaScript with a CSS feature, audit the selector's reach and its neighbors. Constrain `:has()` to direct children with `>`. Use `:where()` to strip borrowed specificity. Check which cascade layer your rule lands in, and whether the competing rules are layered at all. And measure the combined cost of every new rule, not just its isolated impact. A half-layered codebase is harder to debug than a fully imperative one. The same principle applies to Cloudflare's AI Gateway Picks the Right Model for Each Request Automatically, automation that hides its decision logic from the developer creates a system you can't reason about until it breaks. The most honest question a team can ask isn't "can CSS do this?" but "will we still understand what this code does six months from now?"

From Articles on Smashing Magazine — For Web Designers And Developers

Have you ever replaced fifty lines of JavaScript with a few lines of CSS, merged the PR, and felt like you just did the codebase a favor? You replace ResizeObserver with a container query, the diff is mostly red, and everyone approves it before lunch.

This was what it looked like before you made the change:

Read the original at Articles on Smashing Magazine — For Web Designers And Developers