10th Jul, 2020
NoteThis post is almost 5 years old now (at the time of writing this), and although Styled Components are still a viable solution for a lot of CSS issues in React applications; advances in meta frameworks have introduced a lot of complications, and Tailwind might be much simpler approach. Just a heads up!
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.
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.
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.
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.”
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.
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.
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.
As styles are bundled with the component, if the component itself is not used, the CSS is not included, creating painless CSS maintenance.
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.
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.
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.
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).
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.
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.
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.
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.
There are many more CSS-in-JS to consider for yourself here. For the examples below, I’ll be using 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.
Here’s a quick interactive comparison in CodeSandbox:
Like any solution to complex problems, it’s not without a few drawbacks.
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).
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.
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 are different solutions for this).
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.
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.