The Problem with CSS-In-JS, circa Mid-2016

First – this is not meant to be a takedown. These new encapsulated CSS tools are fantastic. We use one. You should too.

I use “CSS-in-JS” here to refer to a family of different techniques ranging from CSS Modules to inline styles. These techniques would better be described as “encapsulated styling”. They differ a lot in syntax and even language, but at their core they give you similar benefits – modular, encapsulated styling.

As it stands, though, these tools share a common weakness in that their modular styles are too tightly encapsulated. This leads to the problem.

The Problem

Some styling properties that semantically belong to the parent syntactically go on the child.

Let’s unwind that gratuitous use of fifty-cent words with some examples from Bootstrap. Consider a button:

Simple enough, right? Create some sort of style object or CSS module for the button, and that’s fine. Need additional style variants? Define them in the same module, and you’re good:

In all of these cases, encapsulated styles are a pure win. You get a cleaner API by defining a <Button> component, exposing all styling options as props, and limiting the potential of other modules to mess with a global .btn class.

Here’s where the problem shows up. What if you need a button group?

Or a button toolbar?

Or you want the button to show up in an inline form?

In many of these cases, you have to do things like tweak the the spacing between the buttons. It’s not the same in each of these contexts.

Critically, though, while that spacing has to be applied as a margin to the button element, it’s not semantically a property of the button itself. It’s semantically a property of the button group, or toolbar, or inline form, or button group in a form, or wherever the button is rendered.

That’s the problem. When you’re writing normal CSS, you can write:

.btn-toolbar > .btn {
margin-left: 0 0.5rem;
}

When you fully encapsulate your styles, such that the equivalent of .btn is not available as a selector, this pattern is no longer available.

Workarounds

What are some ways to deal with this in the CSS-in-JS world?

The first and most obvious one is to just make a <ToolbarButton> component, an <InlineFormButton> component, a <ModalFooterButton> component, and so forth.

The problem is that this approach makes it hard to also use your component hierarchy to express semantic intent. What if you have a <NewWidgetButton> that you then need to use in a toolbar? It’s not helpful to have to add a <NewWidgetToolbarButton>, or a <NewWidgetInlineFormButton>, or to otherwise create a new component every time you have to use the same semantic element in a different context. It’s poor DX – I shouldn’t have to define a new component just to use an existing button in a different context, when the general concept I want to capture is that buttons in that specific context should have a bit more spacing, or something similarly trivial.

You can also use techniques like React context or injecting props to communicate to components how they are rendered, but it runs into an impedance mismatch. The styling we’re talking about here is semantically a property not of the button itself, but of e.g. the button toolbar. It’s the wrong solution to the expression problem to have to tweak code for a child element when the conceptual concern is at the level of the parent element. The spacing between buttons in a toolbar is not a property of buttons – it’s a property of the toolbar.

Lastly, with CSS Modules specifically, you can work around these problems with selectors like:

.btn-toolbar > * {
margin-left: 0 0.5rem;
}

This is equivalent to the <ButtonToolbar> component injecting down an extra CSS class or specific styling overrides to its children.

This might be the best available workaround for now, but it breaks down in places like the inline form example above, where you may want different tweaks to margins for different pieces of that form. And in cases where these overrides get compiled to CSS classes, also brings back the problems with CSS precedence that we were trying to avoid in the first place.

The Solution?

I don’t have one. If I did, this would be a PR or a repo.

Getting rid of selectors altogether throws out too much of the baby with the bathwater. For a lot of use cases, having selectors is helpful. At the same time, making every class and every property be targetable for overrides from parents is a recipe for headaches.

There’s a fine line to draw here. Encapsulated CSS brings huge benefits in limiting the API surface area, but the approaches currently available go so far that they make real use cases difficult. Perhaps we need a concept of specific overridable hooks that allow styling a specific subset of properties.

There’s very good discussion for sorting this out in the context of CSS Modules: https://github.com/css-modules/css-modules/issues/147. I don’t know what it looks like elsewhere, but it’s a problem that’s worth thinking about.


I owe thanks to Jason Quense for working through this argument with me, and to everyone who’s helped read over this piece.