Lack of modules

Lack of modules

Image for post

CSS-in-JS refers to a collection of ideas to solve complex problems with CSS. Since it is NOT a particular library, different libs might solve a different subset of problems and use different approaches, depending on their implementation details.

However, all implementations have in common that they tackle the problems using APIs instead of convention and they leverage JavaScript as a language for styles authoring.

If you are interested in learning the tradeoffs of CSS-in-JS approach, I wrote a separate article for that.

CSS historically never had actual modules, neither did JavaScript. Requirements for web applications evolved and JavaScript has added a module system. First in the form of a bolt-on solution (CommonJS), later as a standard, statically analyzable module system known as ECMAScript Modules (ESM).

The reasoning behind modules applies to both JavaScript and CSS: to be able to hide implementation details by exposing only public APIs. We need to be able to decouple subsystems of an application explicitly so that changing code becomes more predictable.

Not every application needs this, but it makes maintaining medium and large applications easier, simplifying modification and deletion of internal implementation details. Problems appear with more complexity, the smaller the application the less complexity it usually contains.

CSS-in-JS relies on JavaScript?s modules implementation.

Lack of scoping

We know CSS always had a single global namespace, for example, a class can be added to any element, a tag selector can target any element in the document. CSS was initially created to style documents and there was no need for components. The entire page was styled as one big chunk and it usually didn?t involve many people working on it. Since then the complexity of many sites has dramatically increased and this is the main reason why many CSS methodologies were created. None of the conventions is easy to establish and consistently enforce when many people contribute to a project over the years.

Modern websites are complex enough to need many front-end specialists working in separate areas of the site. Those parts are reused across the global site in different ways requiring those blocks to be fully interactive and functional.

The consequence of not having a consistent scoping ? styles leaking in an unpredictable manner.

Here is a simplified example of how CSS-in-JS libraries generate a selector:

const css = styleBlock => { const className = someHash(styleBlock); const styleEl = document.createElement(‘style’); styleEl.textContent = ` .${className} { ${styleBlock} } `; document.head.appendChild(styleEl); return className;};const className = css(` color: red; padding: 20px;`); // ‘c23j4’

CSS-in-JS automates the scoping by generating unique selectors.

Implicit dependencies

CSS offers a rule level code reuse, which means to reuse a style block, a rule has a selector. When selector applies to an element, it applies its entire style block. This is possible in essentially two ways:

1. A CSS rule includes multiple selectors to target different HTML elements.

2. Multiple class names or other attributes are applied to HTML elements, causing them to be targeted by multiple CSS rules.

Neither of those ages well, because both lead to monolithic code structure, where everything depends on everything. It becomes hard to clearly isolate the subsystems.

In the first case, when we add many selectors to a single CSS rule, the rule gets the references to other subsystems. You won?t be able to change those subsystems without touching that rule and it is easy to forget.

In the second case, where we use multiple class names or other attributes, this causes the element to be targeted by multiple CSS rules. This makes the relationship complex again, and again it is easy to forget what needs to be removed.

In both cases, we create dependencies which are hard to understand and change over time without a good strict system in place. It is hard for humans to be that consistent over time.

CSS-in-JS makes dependencies explicit because variables always reference the value in code visually. They are traceable because we can statically analyze where the value comes from. They are granular because we can reuse CSS values, properties or entire style blocks as we see fit.

CSS-in-JS promotes explicit, traceable and granular dependencies.

Dead Code

Due to the implicit relationship between HTML and CSS, it is generally hard to track down unused CSS rules and inform the author or strip them from the bundle. Often you can?t know where the rules have been used. For example, it could be multiple code bases or class names could be conditionally applied from a language that was used to generate HTML or class names have been manipulated by client-side JavaScript.

Over time dead code may have a negative impact on the site performance and the ability of developers to understand the code.

Thanks to explicit, traceable variables and modules in JavaScript, we can implement solutions on top, which create an explicit connection between a CSS rule and the HTML element.

CSS-in-JS helps with removing dead code.

Non-deterministic source order specificity

If you build a Single Page Application (SPA) and you split the CSS bundle per page, you can end up with non-deterministic source order specificity. In such situations the order in which CSS is injected depends on user actions, causing selectors to apply unpredictably to the HTML elements.

Imagine you load page A, and then you switch to page B without a full document reload. Technically you loaded CSS-A and then CSS-B. If selectors used in CSS-B supposed to override selectors from CSS-A we are good because CSS for B was loaded later and has a higher source order specificity.

If the next user comes from a link directly to page B, and then switch to page A, CSS-B will be loaded first, and CSS-A afterwards, causing CSS-A to have a higher source order specificity. Now you have to create visual regression tests for any possible navigation among entry points.

To solve this, it helps to tightly couple CSS and HTML, so that we always know what CSS is used by the currently rendered HTML.

CSS-in-JS helps to avoid non-deterministic source order specificity.

One-to-many relationship

The idea to separate the concerns based on a language ignores the fact that CSS was not designed to be truly separated from HTML.

CSS has implicit assumptions about the HTML structure. For example, flexbox layout makes an assumption that containers to position are direct children of the element it was applied to.

When a CSS rule is applied to different HTML elements across our application, we can basically describe it as a ?one-to-many relationship?. If you change the CSS rule, you potentially need to modify all related HTML elements.

CSS-in-JS encourages this relationship to be one-to-one, while still keeping the ability to have shared properties. Currently not every CSS-in-JS API enforces one-to-one relationship because many libs support CSS reuse without the corresponding HTML. We need to make developers aware of this!

It doesn?t matter which abstraction you use or none at all, the best way to share CSS is to share the HTML and ensure the CSS it needs gets rendered automatically.

CSS-in-JS encourages the coupling of CSS and HTML.

Almighty selectors

It is weird because some people refer to CSS as too powerful and some others refer to CSS-in-JS as too powerful regarding a level of abstraction.

The truth is they are both powerful but in different areas. CSS selectors are too powerful because they can target any element across the document. It?s a huge problem since we try to write CSS that has only access to elements within our HTML block or component.

CSS-in-JS helps to constrain that power by scoping its selectors. It is still not a complete solution because those selectors can reach into any child element if a given library supports cascading. It is a good leap forward though towards writing a more constrained CSS by default. Most CSS-in-JS libs support cascading not because its safe, but because it is practical and there are no good alternatives with safety mechanisms so far. Shadow root CSS is still not where it needs to be for mass adoption.

On the other hand, JavaScript is a much more powerful language, because the syntax is more expressive and allows many more patterns and notations. Complex UX logic is often hard to express without having conditionals, functions, and variables. A pure declarative syntax works well when the runtime is highly specialized for the use case, while CSS is used to accomplish a wide variety of tasks.

CSS-in-JS gives the developer more expressiveness while encouraging more maintainable patterns than cascading.

State-based styling

One of the very powerful patterns CSS-in-JS enables is state-based styling. Technically it is usually implemented as a JavaScript function which receives a state object and returns CSS properties. As a result, a CSS rule is generated that corresponds to the state of an element. Compared to a more traditional way, where we build a class attribute containing multiple class names, this has some advantages:

1. Logic responsible for the final CSS rule has access to the state and can be located together with the rest of styles.

2. Logic generating HTML becomes less cluttered by the classes concatenation logic.

Both of these points should make complex state-dependent CSS more readable.

CSS-in-JS gives developers API to describe state-based styles in a better way than using a bunch of conditional class names.

Image for post


I hope I was able to give you a perspective on the core drivers behind the concept. It is not my intention to judge any technology or to give a complete list of features, which vary between implementations. Also, I am not saying CSS-in-JS is the only future we can have, the point is though that if the community doesn?t understand the problems those tools are trying to solve, how are we going to move forward?


No Responses

Write a response