“In what way is JS any more maintainable than CSS? How does writing CSS in JS make it any more maintainable?”
Happy to chat about this. There’s an obvious disclaimer that there’s a cost to css-in-js solutions, but that cost is paid specifically for the benefits it brings; as such it’s useful for some usecases, and not meant as a replacement for all workflows.
(These conversations always get heated on twitter, so please believe that I’m here to converse, not to convince. In return, I promise to listen to you too and change my opinions; I’ve had mad respect for you for years and would consider your feedback a gift. Also, some of the stuff I’m writing might seem obvious to you; I’m not trying to tell you if all people of some of the details, but it might be useful to someone else who bumps into this who doesn’t have context)
So the big deal about css-in-js (cij) is selectors.
The biggest win from cij is that computers generate selectors for you automatically. While strategies like oocss, etc are conceptually great, relying on selectors to enforce those architectures are hard in a global namespace because there is always a chance of a clash; if not now, 3 years in the future when the team has changed and become bigger and whatnot. This is exacerbated with third party libraries. (In fact, scoping selectors is pretty much the only thing css modules does, and it’s hugely popular and useful for just this.)
Further, it’s extremely hard to statically analyse selectors to see whether any rules are broken (or even a typo!) this means constraints have to be enforced via code reviews and education, which is a harder problem to solve and scale. We should use a computer's assistance us on this, and not rely on humans being infallible (they’re not!)
The enforced control over selectors enables some new capabilities that we couldn’t do easily before; eg - we can trivially extract critical css for html by matching the ids to rules, meaning we can load a page with just 1-2k of css that the page needs for initial render. With no runtime! Frameworks like gatsby and next utilise this heavily for the kind of performance numbers they boast of. Inlining that critical css sirectly into the html is also simple now because it’s so small, removing a whole blocking request for the page. Amazing for boosting time to first render/interactive. This also alleviates many concerns about the js bundle size! The savings come from this critical css approach, along with using dynamic
import()s and code splitting and actual dead code elimination (as opposed to append-only stylesheets that grow over time - some real data here https://twitter.com/mrmrs_/status/874336748528177152?s=21)
Similarly, theming. You know how css vars, despite being so fun, never really took off for the usecases it was designed for, simply because fallback strategies for older browsers are either too hard or limiting? It’s meant that they’ve been usually used for top level variables, but rarely as generic variables to be filled in whenever. Runtime evaluation means you can mix and match js variables in their place, and truly achieve that dream.
In fact, you’ll love this; with this approach, you can truly, finally, polyfill css variables for all browsers. ie - zero runtime css vars for browsers that support it, and a special runtime build for older browsers (I wrote about it in a little more detail here https://gist.github.com/threepointone/0ef30b196682a69327c407124f33d69a and iiuc, this is the only lib that’s ever managed to do this.)
In an SPA/component world, where assets like scripts and styles are loaded asynchronously, you can’t guarantee style sheet loading order, which means you have to lean on some form of runtime composition to guarantee a shape, else you end up with
!important rules even if you follow a given architecture. To clarify - if you have
class=“a b” on an element, and
b are defined in different stylesheets, you can’t be certain of the order of the cascade. The facebook codebase has thousands of
!important statements, despite being written by competent engineers with solid engineering practices and deep relationships with design teams.
You mentioned treating css like js - consider that 10 years ago, we were solving similar problems for js - libraries and modules were registering themselves into the global namespace (
$, etc), and we had to be very particular about the order that script tags were included into html. But we didn’t rely on convention forever - we eventually moved to modules, and build systems to stitch them together to guarantee order and such. These libraries simply enforce this invisibly.
(One of my observations was that you can still actually implement css architectures (like oocss, smacss, etc in the cij world - elements from those architectures are represented as objects instead of selector+rule units. I use this approach and it serves me well. Wrote something on it here - https://gist.github.com/threepointone/9f87907a91ec6cbcd376dded7811eb31)
Now I’m well aware of the drawbacks of css-in-js. Indeed, that’s why there’s no “canonical” library that truly represents css in js - it’s a spectrum of libraries with completely vanilla static css on the one end, and completely dynamic libraries like styled-components on the other end). (Ps- preprocessors like
less feel orthogonal to this spectrum imo, since they can theoretically be used with any of these libraries as an authoring concern.) Each of these libraries have tradeoffs - some do static extraction so there’s no runtime cost, some are tuned for correctness, some for the “developer experience”, others to efficiently do difficult animations, etc etc. This is a reaction to the industry's need for solutions for different usecases, in a hyper-evolving industry. Not just these libs! Even standards folks (via shadow dom etc) are making a valiant effort to solve some of these problems, and it has its own set of tradeoffs, least of which is that it isn’t widely available yet, and the buy in is not acceptable by many teams.
I used to feel very strongly about this, but I’ve tempered my thoughts about this over the last few months. It turns out that the “truth” really lies somewhere in the middle - it’s a function of teams, needs, time, documentation, and so much more. Further, I don’t even think these represents the “final form” of the idea. We should be encouraging of experimentation in this area, if only to learn what we could do better, and maybe even land some of these ideas in browsers.
I’ve written all this out on a phone in the middle of the night, so it might have mistakes/assumptions that I got wrong, but I’m happy to correct and revisit. But I do hope I’ve shown you some perspective to why some teams would use this family of approaches.