What is it? Please use an analogy
Once upon a time, there were three kingdoms: The Kingdom of HTML, the Kingdom of JS, 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 JS 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 JS 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 JS 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 JS were doing.
For a long time, CSS had used tools such as Sass 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 JS was making.
Essentially, in large projects (with many developers working simultaneously), CSS quickly becomes large, bloated and difficult to manage. With interactive designs and the HTML/JS that builds it changing rapidly; CSS gets created and often never used, increasing download times and the perceived page load. It’s often too time-consuming to remove the unused CSS — and more to the point, it was often difficult to know where the CSS was being used. The problem with Cascading Style Sheets is that they Cascade.
Problems with Sass/SCSS
Compared to the vanilla CSS of yesteryear, SCSS felt superpowered. It allowed developers to use more programmatic approaches to maintaining the code, utilising 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.”
- 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.
- 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__gradchild) 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
In the same way that JS has allowed HTML structure to rapidly change on-the-fly (without a slow, awkward server call and page refresh), JS can also be used to quickly change out what CSS is required at any given time. By more tightly coupling HTML, JS and CSS for a particular element, that element becomes cleaner, more adaptable and reduces the effort in maintaining it.
- 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 JS.
- 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.
- 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 – SCSS 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 JS 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 JS allowing for reduced brain squeezing when swapping between language or tools
- Easier SSR – server-side rendering is paramount for the SEO score of SPAs, 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, already 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 SASS background. Currently the most popular solution
- Emotion – performant CSS-in-JS option, though requires CSS to be written closer to inline JS 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 JS/TS itself) — the good news is that writing CSS with styled-components isn’t all that different to writing it in SCSS; though 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; though styled-components does provide an API to reuse chunks of CSS.
How does it compare with regular HTML/CSS?
A quick interactive comparison in CodeSandbox:
Click above for an interactive demo in CodeSandbox:
Cons of styled-components
Like any solution to complex problems, it’s not without a few drawbacks.
- More JS – though the styled-components package itself is fairly small (about 13kb gzipped), styles for each component are now written in JS, 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 JS-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 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.
It’s natural for developers to be suspicious of change in an area that’s largely been static since the widespread adoption of SCSS, but despite some drawbacks listed above; CSS-in-JS provides numerous advantages over other CSS management solutions; from using all the features of JS/TS, 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.