The Case For CSS-in-JS
CSS-in-JS is the practice of utilising the power of JavaScript to dynamically generate and better organise your application’s CSS. The concept has gained traction over the years due to the popularity of UI frameworks / libraries such as React, Angular and Vue. This post attempts to convince you that CSS-in-JS is an approach worth investigating in the struggle to keep your codebase’s CSS in check.
What is it? Please use an analogy
Once upon a time, there were three kingdoms: The Kingdom of HTML, the Kingdom of JavaScript, and the Kingdom of CSS. Although these kingdoms were reliant on each other, they were kept separate; all while providing their talents to the wider world (the browser). Soon though, as the requirements of their citizens grew in complexity, the three kingdoms realised that they would need to form a stronger alliance to secure a more prosperous future for all.
One day The Kingdom of JavaScript said to The Kingdom of HTML: “Hey, you know, we’ve grown quite powerful, and we’d like to provide you with better ways to manage your Kingdom. Whenever you require change, you can use the power of JavaScript to quickly adapt parts of your Kingdom—you no longer need to rebuild everything!” “Oh that’s great!” replied The Kingdom of HTML, and so the two Kingdoms formed a stronger alliance and everyone lived happily ever after.
But what about CSS?
Ah yes, CSS. While HTML and JavaScript were happy living in an ever-evolving, declarative utopia, CSS struggled to keep up. It was CSS’s job, after all, to visually present everything that HTML and JavaScript were doing.
For a long time, CSS had used extensions such as SCSS to give itself more power (variables, mixins, extends, etc.), and methodologies like BEM (.my-component__heading---large
) to better organise itself, but it wasn’t enough to keep up with the progress JavaScript was making.
Essentially, in large projects (with many developers working simultaneously), CSS quickly becomes bloated and difficult to manage. With interactive designs—and the HTML & JavaScript that builds it changing rapidly—CSS often gets created and never used, increasing download times and the perceived page load.
It’s often too time-consuming to remove the unused CSS—as it’s often difficult to know where the CSS was being used. The problem with Cascading Style Sheets is that they Cascade.
Problems with SCSS
Compared to the vanilla CSS of yesteryear, SCSS felt superpowered. It allowed developers to use more programmatic approaches to maintaining the code. They could use variables, functions (mixins), operators and nesting (things which modern vanilla CSS mostly provides natively, btw). But as the saying goes: “With great power comes a greater way of making things difficult for yourself.”
SCSS ughs
- CSS bloat: mixins provide a convenient way to generate code duplication
- Nesting abuse: nesting can be overused and makes it difficult to understand the context of the CSS you’re writing
- Extends abuse: misuse of extends can lead to creating huuuge comma separated class lists
- Compile-time uncertainty: SCSS converts to browser-readable CSS during application compilation, during which time it can be difficult to know in which order CSS will get… ordered, causing specificity problems
- Manual importing: developers need to know which SCSS files to
@import
for external references within a SCSS file to work—else expect node-sass compilation errors - Lack of tree-shaking: SCSS’s
@import
makes all items global, making it difficult for compilers to know where a section of CSS exists. Each stylesheet is executed and its CSS emitted every time it is@import
-ed, which increases compilation time and produces bloated output - Lack of IntelliSense and traversability: with modern tools like TypeScript providing immediate information about each section of code (the type of values a variable holds, the arguments a function takes, etc.), SCSS provides little without additional plugins and configuration
Problems with BEM
BEM (block__element--modifier
) exists to provide context to the developers to understand the element’s place in the HTML hierarchy. It was created as a way of better linking HTML/CSS assets but is prone to developer interpretation which leads to convoluted namings, manageability issues and eventual collapse.
BEM yucks
- Brittle naming: providing unique, descriptive names to classes can be difficult due to similar components that exist or may exist in the future
- Long names are long: class names can get extremely long, and given there’s no upper limit to the amount that can be used, it can take up large amounts of space both in the HTML structure and the corresponding CSS classes, particularly with SCSS usage
- Multiple levels: components with levels of depth of more than parent > child leads to a break in naming conventions (i.e.
.parent__child__grandchild
could become.parent__grandchild
) Multiple classes - in complex applications, there’s often need for multiple BEM classes on a single element, increasing the size of the CSS & HTML output further
The case for CSS-in-JS
JavaScript has allowed HTML structure to change on-the-fly (without a slow, awkward server call and page refresh). In the same way, JavaScript can also be used to quickly change out what CSS is required at any given time.
By more tightly coupling HTML, JavaScript and CSS for a particular element, that element becomes cleaner, more adaptable and reduces the effort in maintaining it.
Selling points
Tighter, locally scoped CSS coupling
Rather than have styles which apply to multiple elements (elements that will evolve in different directions over time), applying styles directly to the component reduces the scope of those styles, giving the developer the confidence that these styles are not used elsewhere. Potentially a 1:1:1 relationship between HTML, CSS and JavaScript.
Reduced CSS overhead
As styles are bundled with the component, if the component itself is not used, the CSS is not included, creating painless CSS maintenance.
Using all the tastiness of JavaScript/TypeScript
Developers can write and manage CSS using all the tools and features made available with JavaScript and TypeScript, rather than getting by with the improvements SCSS originally provided over CSS.
No more worrying about BEM naming
As styles are generated client-side and respond to the HTML rendering, CSS class names are auto-generated, saving developer time worrying about unique CSS naming as well as confusion if a CSS class no longer reflects the element it covers.
Theming and Context
In the case of React, developers can pass Context-ual information down through multiple levels of the component tree. This means global styles can be passed to each component without any manual imports from developers. I.e. padding, colours, font names or any text-based data can have one source of truth which requires zero work to access across the application.
IntelliSense & AutoComplete
CSS provides no immediate feedback that the mixin or variable they’re referencing exists. With CSS-in-JS (particularly in TypeScript) the developer immediately knows if they’re using the reference correctly, and can also AutoComplete CSS properties (as well as custom properties if they’ve been pre-typed within the Theme context).
Portability & Reusability
As all required styles are encapsulated with the HTML and the JavaScript logic, CSS-in-JS styled-components can be easily moved around and duplicated without requiring thought for the location or level in which they render.
Reduced cognitive load
Developers get to use the same tech to write all HTML, CSS, and JavaScript allowing for reduced brain squeezing when swapping between language or tools.
Easier SSR of styles
Server-side rendering is paramount for the SEO score of single-page applications, and most CSS-in-JS solutions provide a clean, reliable approach to achieving this.
CSS-in-JS options
As CSS-in-JS has been a consideration for applications for a while now, there are multiple widely used, mature options to choose from. These options are currently available for React applications but may also have ports to support other frameworks.
- styled-components: easily write CSS-in-JS using much of the same syntax that developers are familiar with coming from a vanilla CSS or SCSS background. Currently the most popular solution
- Emotion: performant CSS-in-JS option, though requires CSS to be written closer to inline JavaScript styles.
- Aphrodite: a platform-agnostic CSS-in-JS solution, though not as widely used as the above
There are many more CSS-in-JS to consider for yourself here. For the examples below, I’ll be using styled-components.
So how do you use styled-components?
Save for some advanced features (and using JavaScript/TypeScript itself), the good news is that writing CSS with styled-components isn’t all that different to writing it in SCSS. Although there are a few things to consider.
The main difference is understanding that the CSS you write will be coupled to a component, rather than being able to be applied to different components. Although styled-components does provide an API to reuse chunks of CSS.
How does it compare with regular HTML/CSS?
Here’s a quick interactive comparison in CodeSandbox:
Cons of styled-components
Like any solution to complex problems, it’s not without a few drawbacks.
More JavaScript
Although the styled-components package itself is fairly small (about 13kB compressed), styles for each component are now written in JavaScript and are compiled in the browser (when not rendered server-side).
Change from traditional CSS
Reading CSS inside a styled-component doesn’t need to look different to normal CSS (or SCSS) but given the advanced features available, CSS can easily look less recognisable and provides a steeper learning curve to less JavaScript-focused developers.
Harder to locate local CSS
If class names are automatically generated, they won’t relate to the code the developer is looking at locally, making it harder to associate where CSS will live (though there different solutions for this).
Coupling styles with a UI framework
In the example of styled-components, it is focused on React applications, meaning those same styles are not as easily shared with non-React applications. Though using an agnostic CSS primer like design tokens would mean that regardless of the framework or CSS-in-JS option, they would consume the same core styles.
Summary
It’s natural for developers to be suspicious of change in an area that’s remained largely static since the widespread adoption of SCSS. However, despite some drawbacks listed above, CSS-in-JS provides numerous advantages over other CSS management solutions: from using all the features of JavaScript/TypeScript, to reducing CSS bundle sizes, to automatic CSS housekeeping.
For organisations already invested in frameworks such as React, it makes sense to harness that declarative flexibility to drive both HTML and CSS rendering, reducing the load on developers and speeding up the user experience.
Enjoyed that? Read some other posts.