Skip to content

Instantly share code, notes, and snippets.

@threepointone
Last active August 26, 2023 15:43
Show Gist options
  • Save threepointone/731b0c47e78d8350ae4e105c1a83867d to your computer and use it in GitHub Desktop.
Save threepointone/731b0c47e78d8350ae4e105c1a83867d to your computer and use it in GitHub Desktop.
For Snook

https://twitter.com/snookca/status/1073299331262889984?s=21

‪“‬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 a 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 sass and 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.

@pluma
Copy link

pluma commented Dec 14, 2018

I too have been making websites since the mid-90s (long enough to remember being excited about the CSS Zen Garden). I've also used SMACSS. These days I identify as a "full-stack" developer but that doesn't mean I don't know what I'm doing (I just had to ping-pong all over the stack throughout my career). I'd like to make the case that the main benefit of CSS-in-JS isn't actually technical, it's social.

Today CSS-in-JS no longer means manipulating inline styles, it mostly means generating stylesheets dynamically and colocating CSS rules with JS components. Depending on the flavor this may indeed mean plain JS objects with camel-cased properties, or it may mean actual CSS code (with nested selectors) in JS template literals. In many ways, CSS-in-JS is an evolution of patterns like SMACSS or BEM for interactive components (i.e. components defined in terms of both of CSS styling and JS behavior).

The big problem with SMACSS and BEM is that they both still require humans to come up with globally unique names. This means generic component names like "Button" or "Header" quickly become misleading in any but the most cookie-cutter designs. What may start out as "the Button" may soon become just "the button we use on the profile page", as opposed to "the new button we now use everywhere else" (but not on the profile page because that button needs to look a certain way for arbitrary business reasons).

The obvious solution is to rename the old component into something more specific so the new component can have the more generic name but that requires a global rename across CSS, HTML and JS (plus templates and possibly across multiple projects) and this can easily go wrong in many ways (not to mention having to communicate this change across possibly multiple teams).

The alternative is "just picking better names" but the actual use of a component may change over time and it may not always make sense to reuse the existing component even when the new component really looks like it's just a variant of the old one.

When developing the JS side of components it really doesn't matter what the components are called and that's a good thing: this makes it possible to refactor JS code to merge convergent components or split divergent variations of one component into separate components, the implementation of the component is mostly an implementation detail.

Alternatively you can use a system like SMACSS alongside a JS component library and just map the component names between the JS/DOM and CSS side but aside from the overhead of always having to look in multiple places this still requires manually naming things and avoiding global naming conflicts.

Even if we disregard the problem of having a single global namespace (which btw even Web Components seem to be beginning to work around lately by letting the user decide what name to register a component with) naming things is hard. Having a human come up with nonsense like "mediabox_widget_inner_wrapper" (or overly specific rules like .mediabox > div > div > div that make the component's inner structure a nightmare to refactor) doesn't provide any benefit compared to just having a CSS-in-JS library call it "MediaBox_a23bef6" (especially if some of the people who have to name things are non-native speakers and may struggle coming up with yet another synonym for the word "wrapper").

Before someone points it out: yes, CSS modules solve some of the same problems. There are technical differences (e.g. without being able to rely on CSS custom properties it's easier to share variables in CSS-in-JS than when using CSS modules) but for most intents and purposes I think it's valid to think of CSS modules as a special case of CSS-in-JS (that it uses a separate CSS file rather than colocating the CSS rules in the JS file is an implementation detail).

@itzikbenh
Copy link

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

And this is where a tool like TailwindCSS comes in handy.
The utility classes approach brings the benefits of both, with zero of the downsides.

@mmmeff
Copy link

mmmeff commented Dec 14, 2018

Awesome summary, thanks so much for writing this.

@w-biggs
Copy link

w-biggs commented Dec 14, 2018

Yeah, I feel like it has very little to do with JavaScript as a language or anything like that - it's the compartmentalization of styling that makes it easier to maintain. Using something like BEM solves the same problem, and IMO it's a matter of preference whether you prefer a CSS methodology, CSS-in-JS, CSS Modules, etc.

@imrvelj
Copy link

imrvelj commented Dec 14, 2018

Gold!

@morewry
Copy link

morewry commented Dec 14, 2018

Good job on this!

@bengfarrell
Copy link

bengfarrell commented Dec 14, 2018

Thanks for the amazing write-up! I've been eyeing CSS-in-JS myself, but not sure if it solves problems for my specific needs. I've been working with Web Components and the Shadow DOM. Both seem to solve the same major problem if I read this right: CSS encapsulation. I'd love to know, for Web Components, if people see a use case for CSS-in-JS. I think making some sort of CSS-in-JS design system would make it more friendly to use as everything is a module and importable, but beyond that I don't know that theres anything compelling for Web Component users. I'm only making this statement to ask if I'm wrong, I'd love to hear any experiences!

@everdimension
Copy link

everdimension commented Dec 15, 2018

Hello @threepointone! Thank you for your valuable write up.

I always feel a bit surprised and uneasy when I see arguments for css-in-js that are about selector scoping. This problem has been long solved by CSS Modules

Your write up can be divided into 3 points:

  1. selector namespace
  2. analysis of selector usage (and extraction of critical css)
  3. theming

1 and 2 are solved by CSS Modules in pretty much the same way as css-in-js solutions.

So should we consider CSS Modules to be falling into css-in-js category? 🤔
May be! In any case I feel that this approach is not mentioned enough in these discussions.

To me it feels that writing completely valid css is a very strong advantage. And since we have a solution that solves basically all issues that you think css-in-js is good for, then why not use it?
The problem with most css-in-js approaches is that they make up new syntax. Most commonly for pseudoelements, but for other cases, too.

When I'm writing CSS Modules, I'm confident in my css. I'm confident about the syntax I'm writing. And when I'm not, I don't have to go to some library's documentation to check for an edge case, I can just consult CSS spec (or stackoverflow for that matter, the point is that my search would be library-agnostic).


As for css variables (aka css custom properties), I feel that their recent lack of support is one of the true reasons for popularity of css-in-js solutions. But I do not think this argument is valid anymore. People support IE11 less and less, people use other new css features such as grids more and more, and the support is kinda great. Many developers do not need to polyfill css variables already and less and less will have the need further on.


To sum it up, I feel that discussions for css-in-js solutions often "steal" the arguments from css modules and make it seem as if the only solution to avoid global selector names collision is to write quirky css strings inside our JS files. It might not always be quirky and there are definitely some good libs out there, but I want people to be aware that the problem of "global selectors" has been solved long ago and it's possible to enjoy writing pure css in separate files (colocated with your components) without any custom syntax made up by a library.

@threepointone
Copy link
Author

I consider css modules to be well and truly css in js. (Fun fact - one of the creators of css modules, glen, is also one of the creators of styled-components). It matches some key characteristics -

  • rewrites selectors to be scoped
  • usually inlined into js bundles (especially when using import()s
  • exposes/exports a js interface/keys

but it doesn’t solve “all” the things. Specifically, the class=“a b” problem, particularly when the stylesheets are loaded asynchronously. If your setup doesn’t have this constraint, you’re gold.

As for css vars - I use india as a benchmark for whether a feature is ready to be used. caniuse has it at <80%, which disqualifies it for me (and also why I even worked on correctly polyfilling it) further, you can only use css vars for property values, and not whole rule sets. There was a proposal to do so (@apply, I mention it in the linked gist), but that’s dead now.

@ahmadawais
Copy link

That's a great write-up, Sunil! Will be referring people to this gist for when we have the same discussion. 👍

@catamphetamine
Copy link

No, CSS is better than the so-called "CSS-in-JS".

@sonhanguyen
Copy link

sonhanguyen commented Dec 30, 2020

No, CSS is better than the so-called "CSS-in-JS".
@catamphetamine

if you're so insist on the CSS syntax itself then try styled-components. CSS modularity is broken evidenced by the endless stream of CSS methodologies trying to fix it that are still coming until this very day. That's the main motivation for cij, unless you have some argument to counter it?

@kvedantmahajan
Copy link

kvedantmahajan commented Feb 10, 2023

My Takeaway - Go for orthogonal solutions such as SCSS and newer elegance TailwindCSS. Choose from the both two as of now depending on your project/library bundle requirements. Rest of the things are great to read about evolution of CSS-in-JS. While it is true that this gist is bookmarked for lifetime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment