A Lukewarm Approval of CSS-in-JS

10-20-21 Catherine Meade

CSS-in-JS can be a worthwhile tool for many projects, even from the perspective of someone who is a die-hard SCSS user. Let’s go through an overview of the CSS-in-JS and the pros and cons of using it instead of Sass.

The discussion about CSS-in-JS, and whether or not to use it in your projects, has been active for nearly half a decade now. With the rise in popularity of JavaScript frameworks like React, Angular, and Vue, many organizations are choosing to use CSS-in-JS in production. Huge companies like Reddit, Atlassian, and Patreon have made the move toward a CSS-in-JS solution. Many of our clients at Sparkbox choose a similar approach.

As someone who loves Sass more than almost any other web language, I was hesitant at first to use CSS-in-JS. Wouldn’t it add package weight to my project? Isn’t it over-engineering? A large number of CSS and SCSS developers share these concerns. In fact, many folks who learned to love CSS without JavaScript balk at the idea of CSS-in-JS. And just as many JavaScript developers can’t understand why you wouldn’t want CSS-in-JS.

However, here I am, having used exclusively CSS-in-JS in enterprise design system environments for the last 2+ years. I think it’s a pretty solid choice. Why?

What Is The Problem CSS-in-JS Is Trying to Solve?

Developer complaints about CSS can mostly be combined into one programmatic issue: Everything in CSS is “globally scoped.” This means that all CSS rules are equally accessible by all of the code. CSS relies on the “cascade” part of “cascading style sheets” to determine priority order of rules. If you’ve ever worked through a CSS issue that has to do with “specificity,” “hierarchy,” or “the cascade,” that probably means you had an issue with global scope.

At Sparkbox and in the wider frontend web community, the way we eliminate these types of global scope-related bugs is by sticking to a sincere strategy for organizing our CSS. For example, ITCSS and BEM create pseudo-scopes of layout objects, blocks/components, elements, settings, tools, etc. A BEM (.block__element--modifier) CSS rule is limited to the html element with the .block classname.

Entire design systems and CSS frameworks (like Bootstrap or Tailwind) are created to solve these problems by tying styles to components, tokens, or utility classes. Many CSS-in-JS libraries claim to offer a better way by directly tying the scope of a CSS styling rule to the JavaScript “component” it means to style.

I’m not an expert on every single JavaScript styling library, so take this as you will. The bulk of my experience is with Emotion. It is an excellent tool popular with many of our clients.

What Benefits Does CSS-in-JS Bring to the Table?

CSS-in-JS provides a more “atomic” way to scope styles only to the components that use them. However, it’s a common misconception among CSS developers that this means we’re inlining all our styles. Many libraries compile the CSS from JavaScript files into a unique class name that is added to the relevant tag. For example, the following button code:

 <button
    css={css`
        background-color: rebeccapurple;
        color: white;

        &:focus {
          outline: 2px dotted plum; 
        }
    `}
  >
    Some Button Text 
  </button>

Could compile to something like this in the DOM, no inline styles to be found:

  <button class=”js-generated-CSS-class”>Some Button Text</button>

Scoping your styles specifically to the component level is how CSS-in-JS solves the hierarchy issue. This is why many React (and similar) developers love using it in their codebases; all the styles live in your React component directory directly.

One of our most popular Sparkbox Foundry articles of all time is still a 2014 article by Ethan Muller, Naming CSS Stuff Is Really Hard. CSS-in-JS makes it so you don’t have to name things so carefully. You can still use names if you want, but because of the component-level scope, you can name things willy-nilly without fear of style collisions. It’s possible to use names like “button” or “heading” in multiple files and nothing will break on the page.

Another common misconception about using CSS-in-JS is that it’s a big performance hit. Depending on the size of your stylesheets, it could actually be a benefit. Just like React, only the components rendered on the page will render their CSS. You no longer need to worry about loading excess styles. Likely if you’re using a JavaScript framework, the size of a styling library is rarely going to be large enough to make a difference. However, if it is truly a concern, libraries like Emotion often have some built-in server-side rendering capabilities.

What Problems Does CSS-in-JS Create or Ignore?

One of the bigger pain points of CSS-in-JS is that the code itself starts to look more like JavaScript. This can be hard to overcome if your team is full of CSS or Sass developers who are uncomfortable writing JavaScript. However, this can have a hidden benefit if your team is full of JavaScript devs who are uncomfortable with CSS syntax—but keep in mind that writing quality CSS-in-JS still requires a solid understanding of CSS fundamental skills.

As with all tools, you have to do the thing that is best for your project and team. If using CSS-in-JS makes your brilliant SCSS developer write mediocre CSS-in-JS, you are losing out on brilliant SCSS! However, there are certain CSS-in-JS libraries where the syntax can feel a lot like writing CSS (and even SCSS!). So your team should be able to go back to their awesome selves after an initial period of learning.

Another complication about CSS-in-JS is that converting existing code is likely no small effort. Imagine rewriting and duplicating your entire CSS codebase on a per-component basis. If that sounds terrible, maybe it’s not worth it. Only your team can decide “if the juice is worth the squeeze.”

CSS-in-JS also often makes little sense if your page has limited JavaScript to begin with. The sweet zone for CSS-in-JS is a project built on a JavaScript framework that utilizes a component structure, often React. Vue and Angular both have their own answer to the CSS scope issue. Vue even does something similar baked in with “scoped styles.” Because of that, CSS-in-JS libraries are often built with React in mind. If you’re not using a JavaScript framework, adding a large CSS-in-JS library just to be able to write CSS in .js files is probably over-engineering.

What Library Should I Use?

So you’ve got mostly JavaScript devs on your team building a React application, a prime scenario for CSS-in-JS. Now to choose a library.

The real choice here is to decide if you want to use a “style object” that looks like a JavaScript object, or if you would rather write something that looks more like CSS or SCSS, but with the benefits of CSS-in-JS.

Using a style object has some drawbacks. CSS syntax is not always supported, so rules like media and feature queries might look very different. You’ll also have to rename all CSS properties to be camelCase (for example, font-color becomes fontColor), and the correct translation is not always readily apparent.

CSS string templates are exactly what they sound like: a string template is compiled directly to CSS. Some Sass features are baked in with Emotion and other libraries, like the & selector and the ability to nest components and media queries. Choosing a string template-based library is not only easier for your CSS developers to acclimate to, but it can also be a huge benefit if you choose to transition existing CSS. Just copy and paste.

Once again, Emotion—my old fav here—does allow you the option of either, but it’s not the only library out there. Styled-Components have so many of the same features that I’d call them interchangeable. If bundle size matters, Emotion v10 runs at about 6kb (though Emotion v11 is a cool 216b). Styled-Components adds about 15kb. There’s also CSS Modules (~10kb), JSS (6kb) or React-JSS (20kb), React-with-Styles (8kb), and countless other options.

To get to know the nuance of each library, I’ve found a tool called CSS-in-JS Playground helpful.

Okay, But What About Design Systems?

Most of my personal developer experience comes from implementing large-scale design systems for enterprise organizations. Many of those design systems have been created to support React component libraries, often built in Storybook and delivered to subscribers via npm modules. And this is the top scenario where this SCSS-fangirl supports using CSS-in-JS over SCSS. If you’re thinking about starting a new React-based design system or upgrading a current design system to better support React, CSS-in-JS can be a solid choice.

If your design system looks something like Bootstrap, that is if elements are styled by adding a number utility or token classes, many CSS-in-JS frameworks still support styling with class names. It’s possible to keep your class name-based system in place and convert to CSS-in-JS one component at a time. Additionally, the ephemeral nature of CSS-in-JS means that generated class names may change between builds, so you may wish to utilize a consistent class name system for accessibility anyway.

Shipping a design system with JavaScript functionality or to a JavaScript Framework can be a challenge. I have historically delivered SCSS via npm that had to be linked from the node_modules directly into a base SCSS stylesheet. It’s not a bad solution, but it does open a codebase up to collisions and scope leaks.

With CSS-in-JS, design system library components have all of their styles bundled and isolated with the component JavaScript. They can still be delivered via npm package, but now when a component is included in the page via JavaScript import, all the styles the component needs come with it as well.

CSS-in-JS also allows you to “extend” design system styles into custom components. Instead of SCSS variables, this can be done with JavaScript constants, or objects or arrays made of string templates or style objects. Props can parse component themes, like dark or light for a card or primary or secondary for button styles. Subscribers can even pass custom styles to tweak components or adjust layout in context. It really is a happy path.

Conclusion

I still like writing SCSS better. I still believe many projects that utilize a JavaScript framework and libraries that provide CSS-in-JS are likely over-engineered, sacrificing performance and accessibility for familiar or more interesting development technologies. However, there are just as many projects and teams that strongly benefit from JavaScript frameworks, and even a fair number where CSS-in-JS is a likely “best choice.” As always, only you can make the decision that is best for your codebase.