Wondering what’s even more challenging than choosing a JavaScript framework? You guessed it: choosing a CSS-in-JS solution. Why? Because there are more than 50 libraries out there, each of them offering a unique set of features.
We tested 10 different libraries, which are listed here in no particular order: Styled JSX, styled-components, Emotion, Treat, TypeStyle, Fela, Stitches, JSS, Goober, and Compiled. We found that, although each library provides a diverse set of features, many of those features are actually commonly shared between most other libraries.
So instead of reviewing each library individually, we’ll analyse the features that stand out the most. This will help us to better understand which one fits best for a specific use case.
Note: We assume that if you’re here, you’re already familiar with CSS-in-JS. If you’re looking for a more elementary post, you can check out “An Introduction to CSS-in-JS.”
Most actively maintained libraries that tackle CSS-in-JS support all the following features, so we can consider them de-facto.
All CSS-in-JS libraries generate unique CSS class names, a technique pioneered by CSS modules. All styles are scoped to their respective component, providing encapsulation without affecting any styling defined outside the component.
With this feature built-in, we never have to worry about CSS class name collisions, specificity wars, or wasted time spent coming up with unique class names across theentire codebase.
This feature is invaluable for component-based development.
When considering Single Page Apps (SPAs) — where the HTTP server only delivers an initial empty HTML page and all rendering is performed in the browser — Server-Side Rendering (SSR) might not be very useful. However, any website or application that needs to be parsed and indexed by search engines must have SSR pages and styles need to be generated on the server as well.
The same principle applies to Static Site Generators (SSG), where pages along with any CSS code are pre-generated as static HTML files at build time.
The good news is that all libraries we’ve tested support SSR, making them eligible for basically any type of project.
Because of the complex CSS standardization process, it might take years for any new CSS feature to become available in all popular browsers. One approach aimed at providing early access to experimental features is to ship non-standard CSS syntax under a vendor prefix:
/* WebKit browsers: Chrome, Safari, most iOS browsers, etc */ -webkit-transition: all 1s ease; /* Firefox */ -moz-transition: all 1s ease; /* Internet Explorer and Microsoft Edge */ -ms-transition: all 1s ease; /* old pre-WebKit versions of Opera */ -o-transition: all 1s ease; /* standard */ transition: all 1s ease;
However, it turns out that vendor prefixes are problematic and the CSS Working Group intends to stop using them in the future. If we want to fully support older browsers that don’t implement the standard specification, we’ll need to know which features require a vendor prefix.
Fortunately, there are tools that allow us to use the standard syntax in our source code, generating the required vendor prefixed CSS properties automatically. All CSS-in-JS libraries also provide this feature out-of-the-box.
There are some CSS-in-JS libraries, like Radium or Glamor, that output all style definitions as inline styles. This technique has a huge limitation, because it’s impossible to define pseudo classes, pseudo-elements, or media queries using inline styles. So, these libraries had to hack these features by adding DOM event listeners and triggering style updates from JavaScript, essentially reinventing native CSS features like :hover, :focus and many more.
It’s also generally accepted that inline styles are less performant than class names. It’s usually a discouraged practice to use them as a primary approach for styling components.
All current CSS-in-JS libraries have stepped away from using inline styles, adopting CSS class names to apply style definitions.
A consequence of using CSS classes instead of inline styles is that there’s no limitation regarding what CSS properties we can and can’t use. During our analysis we were specifically interested in:
All the libraries we’ve analyzed offer full support for all CSS properties.
This is where it gets even more interesting. Almost every library offers a unique set of features that can highly influence our decision when choosing the appropriate solution for a particular project. Some libraries pioneered a specific feature, while others chose to borrow or even improve certain features.
It’s not a secret that CSS-in-JS is more prevalent within the React ecosystem. That’s why some libraries are specifically built for React: Styled JSX, styled-components, and Stitches.
However, there are plenty of libraries that are framework-agnostic, making them applicable to any project: Emotion, Treat, TypeStyle, Fela, JSS or Goober.
If we need to support vanilla JavaScript code or frameworks other than React, the decision is simple: we should choose a framework-agnostic library. But when dealing with a React application, we have a much wider range of options which ultimately makes the decision more difficult. So let’s explore other criteria.
The ability to define styles along with their components is a very convenient feature, removing the need to switch back-and-forth between two different files: the .css or .less/.scss file containing the styles and the component file containing the markup and behavior.
React Native StyleSheets, Vue.js SFCs, or Angular Components support co-location of styles by default, which proves to be a real benefit during both the development and the maintenance phases. We always have the option to extract the styles into a separate file, in case we feel that they’re obscuring the rest of the code.
Almost all CSS-in-JS libraries support co-location of styles. The only exception we encountered was Treat, which requires us to define the styles in a separate .treat.ts file, similarly to how CSS Modules work.
There are two different methods we can use to define our styles. Some libraries support only one method, while others are quite flexible and support both of them.
The Tagged Templates syntax allows us to define styles as a string of plain CSS code inside a standard ES Template Literal:
// consider "css" being the API of a generic CSS-in-JS library const heading = css` font-size: 2em; color: ${myTheme.color}; `;
We can see that:
Some things to keep in mind:
The Object Styles syntax allows us to define styles as regular JavaScript Objects:
// consider "css" being the API of a generic CSS-in-JS library const heading = css({ fontSize: "2em", color: myTheme.color, });
We can see that:
Some things to keep in mind:
Now that we know what options are available for style definition, let’s have a look at how to apply them to our components and elements.
The easiest and most intuitive way to apply the styles is to simply attach them as classnames. Libraries that support this approach provide an API that returns a string which will output the generated unique classnames:
// consider "css" being the API of a generic CSS-in-JS library const heading_style = css({ color: "blue" });
Next, we can take the heading_style, which contains a string of generated CSS class names, and apply it to our HTML element:
// Vanilla DOM usage const heading = `<h1 >Title</h1>`; // React-specific JSX usage function Heading() { return <h1 className={heading_style}>Title</h1>; }
As we can see, this method pretty much resembles the traditional styling: first we define the styles, then we attach the styles where we need them. The learning curve is low for anyone who has written CSS before.
Another popular method, that was first introduced by the styled-components library (and named after it), takes a different approach.
// consider "styled" being the API for a generic CSS-in-JS library const Heading = styled("h1")({ color: "blue" });
Instead of defining the styles separately and attaching them to existing components or HTML elements, we use a special API by specifying what type of element we want to create and the styles we want to attach to it.
The API will return a new component, having classname(s) already applied, that we can render like any other component in our application. This basically removes the mapping between the component and its styles.
A newer method, popularised by Emotion, allows us to pass the styles to a special prop, usually named css. This API is available only for JSX-based syntax.
// React-specific JSX syntax function Heading() { return <h1 css={{ color: "blue" }}>Title</h1>; }
This approach has a certain ergonomic benefit, because we don’t have to import and use any special API from the library itself. We can simply pass the styles to this css prop, similarly to how we would use inline styles.
Note that this custom css prop is not a standard HTML attribute, and needs to be enabled and supported via a separate Babel plugin provided by the library.
There are two mutually exclusive methods to generate and ship styles to the browser. Both methods have benefits and downsides, so let’s analyze them in detail.