Andrei Pfeiffer logo
Back to Articles

The evolution of scalable CSSPart 7: CSS-in-JS

CSS
13 min read

The evolution of scalable CSS is a multi-part chronicle intended to document the progress of tools, practices and techniques that enable us to write maintainable CSS at scale.

  1. Introduction
  2. Part 1: CSS scalability issues
  3. Part 2: Good practices
  4. Part 3: CSS Processors
  5. Part 4: Methodologies and Semantics
  6. Part 5: Styles Encapsulation
  7. Part 6: Atomic CSS
  8. Part 7: CSS-in-JS
  9. Part 8: Type-safe CSS
  10. Epilogue

In the previous part of this series, Part 6: Atomic CSS, we've covered an alternative paradigm to CSS authoring, using a predefined set of single-purpose utility classes that we reference in HTML. This paradigm removes the majority of scalability issues associated with Semantic CSS.

During this part we'll cover a third paradigm, introduced around the same period as Atomic CSS. Instead of using .css files for style definition, CSS-in-JS moves the definition inside .js files, colocating the component's styles with its markup and behavior.


We won't be analyzing any particular library as their APIs differ quite a bit. Instead, we'll focus on CSS-in-JS as a paradigm, looking at its fundamental principles and key features to understand the underlying technology. Here's what we'll cover next:


Timeline of scalable CSS evolution, highlighting the CSS-in-JS branch (in yellow) and featuring essential turning points: Atomic CSS-in-JS in 2016 and Static CSS Extraction in 2017
In addition to countless features, specific CSS-in-JS libraries can also output Atomic CSS classes or even generate Static CSS files

Origins

It might come as a surprise, but the idea of "styling with JavaScript" is not as novel as many would believe.

In 1996, Netscape implemented JSSS, aka. JavaScript-Based Style Sheets as an alternative to CSS. Netscape even submitted the specification to W3C for standardization. However, it didn't gain enough traction, as "CSS had a much wider industry acceptance". Ultimately, in 2000, Netscape dropped all support for JSSS.

The birth of CSS-in-JS

CSS-in-JS, as it is known today, was coined in 2014. There are two known pioneers, namely Christopher Chedeau from Facebook and Oleg Isonen the author of JSS (not to be confused with Netscape's JSSS from 1996).

NoteWe should recollect that CSS Modules or other popular style encapsulation approaches were not available at that time. Atomic CSS frameworks were around, but quite exotic and unpopular.

At its core, CSS-in-JS is simply an alternative solution to solve the problems of scalable CSS. As a consequence, there is a considerable amount of overlap between CSS-in-JS and the other approaches covered in previous chapters:

  • Styles encapsulation is a core feature of any CSS-in-JS library. CSS Modules played an essential role in popularizing CSS class hashing, a technique borrowed by most CSS-in-JS libraries to scope their styles.
  • Explicit dependencies enable simplified reasoning about the code because the classes applied in the markup don't reference implicit CSS classes. Instead, there is an explicit reference between the definition of the style and its usage.
  • Contextual styling is another core feature of CSS-in-JS libraries, enabling us to define nested rules, media queries, and pseudos within a single style definition, thus preventing code duplication, similar to CSS Preprocessors.
  • Avoiding zombie code is mainly a consequence of having explicit dependencies because every component defines or imports its styles. Thus, when we remove the component or don't import it anymore, its styles will also be ignored during bundling.

This overlap could explain why so many developers disregard CSS-in-JS from the start. For instance, their problems might be already solved by CSS Modules or other forms of encapsulation in conjunction with a CSS processor.

Beyond static styling

Besides the core features previously noted, CSS-in-JS solves other problems as well, beyond what CSS Modules or CSS processors can achieve, simply as a consequence of using JavaScript files for styles definition:

  • Code navigation features, such as Go to definition or Find all references, are not available with plain CSS. JavaScript-defined styles unlock the power of tools specific to programming languages, significantly improving the development experience.
  • Dynamic styling is prevalent in highly interactive applications. State-based styling like toggling styles or component variations and user-defined styles are trivial and straightforward to implement with CSS-in-JS.
  • Sharing variables from (S)CSS to JavaScript and vice-versa is technically possible but requires additional boilerplate, and it's error-prone. With CSS-in-JS, we can share not only constants or variables, but also types, functions, or any other JS code.
  • Type-checking is not possible with regular CSS. If a style is missing or a class name is misspelled, no tool will help us notice the problem. CSS-in-JavaScript unlocks the power of type-checking, covered in detail in Part 8: Type-safe CSS.

In addition, CSS-in-JS unlocked other features that turned out to be trivial to implement, thanks to having all style definitions in JavaScript:

  • Theming and especially runtime theme toggling is supported by most CSS-in-JS solutions.
  • Critical CSS extraction for Server-Side Rendered pages is implemented in all CSS-in-JS libraries that support SSR.
  • Colocated styles within the component file, similar to React Native StyleSheets, Vue.js SFC, or Angular Components, go hand in hand with component-based approaches.
  • Lazy loading styles comes out of the box with dynamically loaded components.

Now that we have a basic understanding of the theory behind CSS-in-JS let's look at its typical syntax to define styles and attach them to HTML elements.

The basics

To better understand how CSS-in-JS works, let's imagine a fictional library that supports typical CSS-in-JS features. The first thing we'll have to do is to import the styling API, typically a function called css or styled:

import { css } from "css-in-js-library";

Styles definition

Depending on the library's support, the styling function will receive the CSS styles as input, which could be either a JavaScript Object or a Tagged Template string.

JavaScript Object syntaxJS
const title = css({
  fontSize: "2rem",
  color: DARK_BLUE,
});
Tagged Template syntaxJS
const title = css`
  font-size: 2rem;
  color: ${DARK_BLUE};
`;

Most libraries prefer the Object syntax because it's more performant, as Tagged Templates require an additional parsing step from string to object. However, some libraries like Emotion, Goober, JSS, or Compiled are pretty flexible, supporting both syntaxes.

Further readingYou can find more details on the differences between the two methods in the Thorough Analysis of CSS-in-JS.

Styles attachment

Now that we've defined the styles, let's explore how to apply them to HTML elements. There are three different approaches that CSS-in-JS libraries typically support.

Class strings API

The most intuitive and popular API returns a uniquely generated string representing the element's CSS class name.

const title = css(/* CSS rules */);

// ➡️ unique string: "1dbj"

The main benefit of this approach is that it resembles the traditional way of styling. But, at the same time, this method is agnostic of the underlying JS framework, working with any JavaScript framework, or using vanilla DOM APIs:

Using JSX syntaxJSX
export const Page = () => (
  <h1 className={title}>...</h1>
);
Using Vanilla DOM APIsJS
document.body.innerHTML = `
  <h1 class="${title}">...</h1>
`;
Styled component API

A second method, popularized by the styled-components library, which also gave its name, takes a different approach. Instead of defining the styles first and applying them to existing HTML elements afterward, a typical styled API will receive the type of element along with its styles.

const Title = styled("h1")(/* CSS rules */);

// ➡️ new <Title /> component

As a result, it will return a new component with the CSS class already applied. This method works well with component-based approaches, encapsulating the element's definition with its styles and removing the mapping required with standard CSS.

export const Page = () => (
  <Title>...</Title>
);

Consequently, the end result will be the same:

<h1 class="1dbj">...</h1>

Libraries that support the Styled component API include styled-components, Emotion, JSS, Goober, Compiled, and Stitches. It's worth noting that this approach is mostly used with JSX-based frameworks.

css prop API

The third method, introduced by Glamor and popularized by Emotion uses a non-standard attribute, typically named css, to specify the element's styles.

export const Page = () => (
  <h1 css={/* CSS rules */}>...</h1>
);

The css prop API is less prevalent, being supported by fewer libraries, including Emotion, styled-components, Compiled, or Goober. This approach is typically implemented with JSX-based frameworks, where attributes are called props, hence the name of this method.


Styles output

So far, we've only covered a few methods to define and apply the styles to HTML elements. But how does the browser handle the styles written in JavaScript?

As browsers cannot parse CSS defined in JavaScript files, one way to render the styles in the browser is to inject them at runtime. This approach is known as runtime stylesheets, and it's the most popular one among existing libraries.

Additional JavaScript code, called the library runtime, has to be bundled and shipped to the browser for this to work. This code will inject the required styles into the browser and update them accordingly whenever a user event triggers a style change.

Styles defined in JavaScript and applied to component's HTML elements are parsed by the CSS-in-JS library runtime, which generates unique CSS class names, and injects the CSS rules as style tags inside the document's head

The Runtime Stylesheets method is not the only approach. As we'll later see, there are libraries capable of generating Static CSS files that can be referenced like any regular stylesheet. In addition, other solutions can even generate Atomic CSS.

Further readingYou can find more details regarding the style output methods in the Thorough Analysis of CSS-in-JS.

Now that we've covered the basics let's turn our attention to several features that set CSS-in-JS apart from other authoring paradigms.

Dead code elimination

Removing unused code is not limited to CSS-in-JS, as it also works with CSS Modules. However, with CSS-in-JS we get more fine-grained and efficient results:

  • with CSS Modules, dead code elimination works at the component or file level;
  • but dead code elimination works at the element or style level in CSS-in-JS.

Consider the following implementation with CSS Modules:

footer.jsx (with CSS Modules)JSX
import styles from "./footer.module.css";

export function Footer() {
  return (
    <footer>
      <p className={styles.contact}>Get in touch</p>
      {/*
        ✂️ Removed code
        <p className={styles.copyright}>Copyright 2021</p>
      */}
    </footer>
  );
}
footer.module.cssCSS
.contact {
  color: "black";
}

/* 😡 will be bundled, even though it's not used */
.copyright {
  color: "grey";
}

All styles defined in the imported CSS Module will be bundled, even the unused ones. There is no trivial way to ensure that a particular class isn't actually used in the code.

As long as the <Footer /> component is imported somewhere in the application, the entire .css file will also be bundled. However, if the <Footer /> component is not imported anymore, the .css file will also be removed from the bundle as well.


Next, let's look at the same implementation but using CSS-in-JS instead:

footer.jsx (with CSS-in-JS)JSX
import { css } from "css-in-js-library";

const contact_styles = css({ color: "black" });

// 😌 will not be bundled, as it is not referenced
const copyright_styles = css({ color: "grey" });

export function Footer() {
  return (
    <footer>
      <p className={contact_styles}>Get in touch</p>
      {/*
        ✂️ Removed code
        <p className={styles.copyright}>Copyright 2021</p>
      */}
    </footer>
  );
}

With CSS-in-JS, on the other hand, we have a more direct reference between style definition and its usage. In our example, copyright_styles is just a JavaScript constant. Since we don't reference it anywhere in the component code, its styles won't be bundled.

In addition, linters can highlight unused identifiers, which we can safely delete. Therefore, the source code is also easier to maintain.

Shared variables

There are many reasons for sharing custom values between CSS and JavaScript code, even in small websites. On the other hand, larger applications, especially those using a design system, will likely require us to share design tokens.

With CSS-in-JS, we don't have to perform explicit conversions or rely on tools to automate the conversions between (S)CSS and JS variables. Since all styles are defined and applied in JavaScript, they can reference any JS identifiers:

import { css } from "css-in-js-library";
import { theme, desaturate } from "./theme.js";

const COLOR = "#f45919";

const heading_styles = css({
   // 👍 local defined constant
  color: COLOR,
   // 👍 imported design token
  padding: theme.default_spacing,
   // 👍 computed value
  background: desaturate(0.2, COLOR),
});

export function Title() {
  return <h1 className={heading_styles}>Heading</h1>;
}

It's needless to say that sharing any code between logic, styles, and markup becomes trivial with CSS-in-JS.

Code navigation

As we've all probably experienced by now, we read, debug, and change code more often than writing it. Therefore, navigating effortlessly through code is essential, especially with large or unfamiliar codebases.

Consider a simple task as exploring what CSS rules are defined for a particular CSS class applied to an element:

  • Using plain (S)CSS requires us to copy the CSS class, paste it into a search tool and append a dot . so we'll find its definition.
  • With CSS Modules, the process is the same, even though we're explicitly importing the styles.
Go to Definition

With CSS-in-JS, all styles are JavaScript constants. The editor will instantly open the style definition using Ctrl + click (CMD + click on Mac) or selecting Go to Definition.

Find all References

The opposite task is also simple. To discover the usages of a particular style (or any other variable, for that matter), we can Right-click on the identifier and select Find All References from the context menu.


Navigating code easily is not a measurable feature, but once we get used to it and feel its usefulness, it becomes obvious that it dramatically improves the overall development experience.

Static CSS Extraction

There are a lot of complaints about CSS-in-JS regarding its runtime overhead, as it requires additional code to evaluate, inject, and update styles in the browser. Indeed, this overhead varies between 1.1 kB and 18.2 kB gzipped and minified.

However, libraries like Astroturf, Linaria, or vanilla-extract implement static CSS file extraction, generating actual .css files at build time, which are included in our document as any regular CSS Stylesheet.

At build time, Static CSS extraction libraries parse the styles defined in JavaScript and applied to component's HTML elements, generating actual CSS files with unique CSS class names

This technique adds zero runtime cost. Thus, we get all the benefits of CSS-in-JS regarding the development experience and, at the same time, having no runtime cost, similar to regular CSS.

Performance considerationsFewer transferred bytes don't always imply faster load metrics like First Paint or Time To Interactive. Check out this debate regarding performance between Runtime Stylesheets and Static CSS extraction for more details.

Atomic CSS-in-JS

Some libraries like Fela, Compiled, Stitches, and Stylex took CSS-in-JS to another level. Instead of generating CSS classes containing all defined rules, they focused on generating unique and atomic classes.

Atomic CSS-in-JS libraries parse the styles defined in JavaScript and applied to component's HTML elements, generating atomic class names for each unique CSS rule

Now, the beautiful part about Atomic CSS-in-JS is that we don't have to learn the specific set of class names of a particular Atomic CSS framework. Instead, we write the styles like we usually do with CSS-in-JS.

It's the library's responsibility to generate the required atomic CSS classes.

Statically Atomic CSS-in-JS

Before we conclude, we should also mention solutions like vanilla-extract + Sprinkles, or Style9, which combine Static CSS extraction and Atomic CSS, bringing together the benefits of all three paradigms covered so far:

  • the development experience of CSS-in-JS;
  • the small and optimized file size of Atomic CSS;
  • the trivial caching of Static stylesheets.

Criticism

Despite its versatility and potential, CSS-in-JS received a significant kickback from the community. Most of the reasons are misconceptions, but there are two essential dilemmas that we'll eventually encounter with CSS-in-JS:

  1. How to choose a CSS-in-JS library? There are more than 50 different solutions, each offering a unique set of features. Undoubtedly, it's not trivial to understand which of them best fits a particular project. Trying out all of them is a daunting task.
  2. Can we commit to CSS-in-JS in the long term? Choosing a library is only part of the story. But there's no guarantee that a particular popular library today will be maintained 5 or 10 years from now.

If we take a step back, we had similar dilemmas before, with CSS Preprocessors, JavaScript frameworks, state management libraries for React, static type checkers, compile-to-JS languages, etc. But after a period of pioneering, failed experiments, and JavaScript fatigue, the community eventually figured out which solutions worked and which didn't.


Before we move on, let's recap what CSS-in-JS is all about:

  • Styles encapsulation, so CSS Modules, Shadow DOM, or other CSS scoping techniques are not required.
  • Variables and contextual styles render all CSS preprocessors obsolete.
  • Fine-grained dead code elimination successfully removes unused styles at build time out-of-the-box.
  • Powerful code navigation support enables features such as Go to definition and Find all references.
  • Atomic classes eliminate the need to learn and use Atomic CSS or Utility-first frameworks.
  • Static stylesheets remove the runtime cost, providing trivial caching like regular CSS files.

But, there's more. In the final chapter, Part 8: Type-safe CSS, we'll explore the benefits of using TypeScript on top of existing CSS-in-JS solutions, enabling type safety for our CSS code.


References and further reading

Scroll to top