Home

Awesome

A thorough analysis of CSS-in-TS

This document contains an in-depth analysis of all the current CSS-in-JS solutions, that support Server Side Rendering and TypeScript.

The baseline reference we'll use for comparison is a CSS Modules approach.
We're using Next.js as a SSR framework for building resources.
Last important aspect is type-safety with full TypeScript support.

<br />

๐Ÿ—“ Last update: Aug 2021

๐Ÿ—ž To get a shorter overview, you can checkout the article on CSS Tricks:
https://css-tricks.com/a-thorough-analysis-of-css-in-js/

๐Ÿ“ฝ If you prefer a video instead, you can checkout my talk from ngPartyCZ:
https://www.youtube.com/watch?v=c7uWGhrAx9A

โœ‹ Please checkout our goals & disclaimer before jumping to conclusions.

<br />

Table of contents

<br />

Motivation

The CSS language and CSS Modules have some limitations, especially if we want to have type-safe code. Some of these limitations have alterative solutions, others are just being annoying or less than ideal:

  1. Styles cannot be co-located with components
    This can be frustrating when authoring many small components, but it's not a deal breaker. However, the experience of moving back-and-forth between the component.js file and the component.css file, searching for a given class name, and not being able to easily "go to style definition", is an important productivity drawback.

  2. Styling pseudos and media queries requires selector duplication
    Another frustrating fact is the need to duplicate our CSS classes when defining pseudo classes and elements, or media queries. We can overcome these limitations using a CSS preprocessor like SASS, LESS or Stylus, that supports the & parent selector, enabling contextual styling.

    .button {}
    
    /* duplicated selector declaration for pseudo classes/elements */
    .button:hover {}
    .button::after {}
    
    @media (min-width: 640px) {
      /* duplicated selector declaration inside media queries */
      .button {}
    }
    
  3. Styles usage is disconnected from their definition
    We get no IntelliSense with CSS Modules, of what CSS classes are defined in the component.css file, making copy-paste a required tool, lowering the DX. It also makes refactoring very cumbersome, because of the lack of safety.

  4. Using type-safe design tokens in CSS is non-trivial
    Any design tokens defined in JS/TS (to benefit from type-safety) cannot be directly used in CSS.

    There are at least 2 workarounds for this issue, neither of them being elegant:

    • We could inject them as CSS Custom Properties / Variables, but we still don't get any IntelliSense or type-safety when using them in .module.css.
    • We could use inline styles, which is less performant, and it also introduces a different way to write styles (camelCase vs. kebab-case), while also splitting the styling in 2 different places: the component file and the .css file.
    • We could use CSS (or SASS) as the source of truth for design tokens, by storing them as CSS Custom Properties and read them from JS using DOM queries, but we'd still need to manually update both the CSS and JS code when we perform any change, because we don't have type-safety when dealing with CSS;
<br />

Goals

There are specific goals we're looking for with this analysis:

<br />

Getting even more specific, we wanted to experience the usage of various CSS-in-JS solutions regarding:

<br />

Disclaimer

This analysis is intended to be objective and unopinionated:

<br />

๐Ÿ‘Ž What you WON'T FIND here?

<br />

๐Ÿ‘ What you WILL FIND here?

The libraries are not presented in any particular order. If you're interested in a brief history of CSS-in-JS, you should checkout the Past, Present, and Future of CSS-in-JS insightful talk by Max Stoiber.


<br/> <br/>

Overview

1.ย Coโ€‘location2.ย DX3.ย tag`ย `4.ย {ย }5.ย TS6.ย &ย ctx7.ย Nesting8.ย Theme9.ย .css10.ย <style>11.ย Atomic12.ย className13.ย <Styledย />14.ย cssย prop15.ย Agnostic16.ย Pageย sizeย delta
CSSย ModulesโŒโœ…โœ…โŒโŒโŒโœ…โŒโœ…โŒโŒโœ…โŒโŒโœ…-
Styledย JSXโœ…๐ŸŸ โœ…โŒ๐ŸŸ โŒโœ…โŒโŒโœ…โŒโœ…โŒโŒโŒ+2.8ย kBย /ย +12.0ย kB
Styledย Componentsโœ…๐ŸŸ โœ…โœ…โœ…โœ…โœ…โœ…โŒโœ…โŒโŒโœ…โœ…โŒ+13.4ย kBย /ย +39.0ย kB
Emotionโœ…โœ…โœ…โœ…โœ…โœ…โœ…โœ…โŒโœ…โŒโœ…โœ…โœ…โœ…+6.5ย kB / +20.0 kB
TypeStyleโœ…โœ…โŒโœ…โœ…โœ…โœ…๐ŸŸ โŒโœ…โŒโœ…โŒโŒโœ…+2.1ย kB /ย ย +8.0 kB
Felaโœ…๐ŸŸ ๐ŸŸ โœ…๐ŸŸ โœ…โœ…โœ…โŒโœ…โœ…โœ…โŒโŒโœ…+11.9ย kB / +43.0ย kB
Stitchesโœ…โœ…โŒโœ…โœ…โœ…โœ…โœ…โŒโœ…โŒโœ…โœ…๐ŸŸ โœ…+5.3ย kB / +17.0ย kB
JSSโœ…โœ…๐ŸŸ โœ…โœ…โœ…โœ…โœ…โŒโœ…โŒโœ…๐ŸŸ โŒโœ…+18.2ย kB / +60.0ย kB
Gooberโœ…โœ…โœ…โœ…โœ…โœ…โœ…โœ…โŒโœ…โŒโœ…โœ…๐ŸŸ โœ…+1.1ย kB / ย +4.0ย kB
Compiledโœ…โœ…โœ…โœ…โœ…โœ…โœ…โŒโŒโœ…โœ…๐ŸŸ โœ…โœ…โŒ+3.5ย kB / ย +9.0ย kB
Linariaโœ…โœ…โœ…โŒโœ…โœ…โœ…โœ…โœ…โŒโŒโœ…โœ…โŒโœ…+2.7ย kB / ย +6.0ย kB
vanilla-extractโŒโœ…โŒโœ…โœ…โœ…โŒโœ…โœ…โŒ๐ŸŸ โœ…โŒโŒโœ…+0.0ย kB /ย ย -2.0 kB
<br />

LEGEND:

<br />

1. Co-location

The ability to define styles within the same file as the component. Note that we can also extract the styles into a separate file and import them, in case we prefer it.

โฌ†๏ธ to overview

<br />

2. DX

Refers to the Developer eXperience which includes 2 main aspects:

โฌ†๏ธ to overview

<br />

3. tag`ย ` (Tagged Templates)

Support for defining styles as strings, using ES Tagged Templates:

โฌ†๏ธ to overview

<br />

4. {ย } (Object Styles)

Support for defining styles as objects, using plain JavaScript objects:

โฌ†๏ธ to overview

<br />

5. TS

TypeScript support, either built-in, or via @types package, which should include:

โฌ†๏ธ to overview

<br />

6. & ctx (Contextual Styles)

Support for contextual styles allowing us to easily define pseudo classes & elements and media queries without the need to repeat the selector, as required in plain CSS:

โฌ†๏ธ to overview

<br />

7. Nesting

Support for arbitrary nested selectors:

โฌ†๏ธ to overview

<br />

8. Theming

Built-in support for Theming or managing tokens for a design system.
We haven't tested out this feature, so we're only taking notes which libraries express their support in their docs.

โฌ†๏ธ to overview

<br />

9. .css (Static CSS extraction)

Defined styles are extracted as static .css files:

โฌ†๏ธ to overview

<br />

10. <style> tag

Defined styles are injected inside <style> tags in the document's <head>:

โฌ†๏ธ to overview

<br />

11. Atomic CSS

The ability to generate atomic css classes, thus increasing style reusability, and reducing duplication:

โฌ†๏ธ to overview

<br />

12. className

The library API returns a string which we have to add to our component or element;

โฌ†๏ธ to overview

<br />

13. <Styled />

The API creates a wrapper (or Styled) component which includes the generated className(s):

โฌ†๏ธ to overview

<br />

14. css prop

Allows passing styles using a special css prop, similar how we would define inline styles, but the library generates a unique CSS class name behind the scenes:

โฌ†๏ธ to overview

<br />

15. Framework agnostic

Allows usage without, or with any framework. Some libraries are built specifically for React only.
NOTE: some libraries like Stitches or Emotion document only React usage, although they have a core that's framework agnostic.

โฌ†๏ธ to overview

<br />

16. Page size delta

The total page size difference in kB (transferred gzipped & minified / uncompressed & minified) compared to CSS Modules, for the entire index page production build using Next.js:

NOTE: all builds were done with Next.js 11.1.0 and the values are taken from Chrome Devtools Network tab, Transferred over network vs Resource size.

โฌ†๏ธ to overview

<br/>
<br/>

Overall observations

The following observations apply for all solutions (with minor pointed exceptions).

<br />

โœ… Code splitting

Components used only in a specific route will only be bundled for that route. This is something that Next.js performs out-of-the-box.

<br />

โœ… Global styles

All solutions offer a way to define global styles, some with a dedicated API.

<br />

โœ… SSR

All solutions offer Server-Side Rendering support and are easy to integrate with Next.js.

<br />

โœ… Vendor prefixes

All solutions automatically add vendor specific prefixes out-of-the-box.

<br />

โœ… Unique class names

All solutions generate unique class names, like CSS Modules do. The algorithm used to generate these names varies a lot between libraries:

<br />

โœ… No inline styles

None of the solutions generate inline styles, which is an older approach, used by Radium & Glamor. The approach is less performant than CSS classes, and it's not recommended as a primary method for defining styles. It also implies using JS event handlers to trigger pseudo classes, as inline styles do not support them. Apparently, all modern solutions nowadays moved away from this approach.

<br />

โœ… Full CSS support

All solutions support most CSS properties that you would need: pseudo classes & elements, media queries and keyframes are the ones that we've tested.

<br />

โœ… Critical CSS extraction

Most solutions market themselves as being able to "extract critical CSS" during SSR. Please note that this does NOT refer to above-the-fold critical CSS extraction, as we initially thought.

What they actually do:

With 100% static CSS, there would be actually no benefit. With dynamic pages that render very few elements on the server, and most components are rendered dynamically on the client, the benefit increases.

EXCEPTION: libraries that use static CSS extraction.

<br />

๐ŸŸ  Performance Metrics

Understanding how these features affect Core Web Vitals and Performance Metrics in general is an extremely important factor to consider, and the way styles are delivered to the client has probably the biggest impact, so let's analyse this in detail.

Also, there are 2 different scenarios we need to consider:

<br />

1. .css file extraction

Solutions that generate .css static files, which you normally would include as <link> tag(s) in the <head> of your page, are basically rendering-blocking resources. This highly affects FCP, LCP and any other metric that follows.

๐Ÿ“ญ Empty cache
If the user has an empty cache, the following needs to happen, negatively impacting FCP and LCP:

It's true that you can fetch in parallel other <head> resources (additional .css or .js files), but this is generally a bad practice;

๐Ÿ“ฌ Full cache
However, on subsequent visits, the entire .css resource would be cached, so FCP and LCP would be positively impacted.

<br />

๐Ÿ’ก Key points
This solution appears to be better suited when:

<br />

2. <style> tag injected styles

During SSR, styles will be added as <style> tag(s) in the <head> of the page. Keep in mind that these usually do NOT include all styles needed for the page, because most libraries perform Critical CSS extraction, so these styles should be usually smaller than the entire .css static file discussed previously.

๐Ÿ“ญ Empty cache
Because we're shipping less CSS bytes, and they are inlined inside the .html file, this would result in faster FCP and LCP:

๐Ÿ“ฌ Full cache
When the user's cache is full, the additional .js files won't require fetching, as they are already cached.
However, if the page is SSRed, the inlined critical CSS rendered in the <style> tag of the document will be downloaded again, unless we deal with static HTML that can be cached as well, or we deal with HTML caching on our infrastructure.

But, by default, we will ship extra bytes on every page HTTP request, regardless if it's cached or not.

<br />

๐Ÿ’ก Key points
This solution appears to be better suited when:

<br />

๐ŸŸ  Dead code removal

Most solutions say they remove unused code/styles. This is only half-true.

Unused code is indeed more difficult to accumulate, especially if you compare it to plain .css files as we used to write a decade ago. But when compared to CSS Modules, the differencies are not that big. Any solution that offers the option to define arbitrary selectors or nested styles will bundle them, regardless if they are used or not inside our component. We've managed to ship unused SSR styles with all the tested solutions.

True & full unused code removal is difficult to implement, as the CSS syntax is not type-checked, nor statically analyzable. Also, the dynamic nature of components make it practically impossible in certain scenarios, especially when the markup is dynamically rendered:

Basically, what we get is code removal when we delete the component, or we don't import it anymore. That's implicit behaviour, because the styles are a direct dependency of the component. When the component is gone, so are its styles.

<br />

๐ŸŸ  Debugging / Inspecting

There are 2 methods to inject CSS into the DOM & update it from JavaScript:

<br />
1. Using <style> tag(s)

This approach implies adding one or more <style> tag(s) in the DOM (either in the <head> or somewhere in the <body>), using .appendChild() to add the <style> Node(s), in addition with either .textContent, .innerHTML to update the <style> tag(s).

<br />
2. Using CSSStyleSheet API

First used by JSS, this method uses CSSStyleSheet.insertRule() to inject CSS rules directly into the CSSOM.

<br />

โŒ No component deduping

If the same component is imported by 2 different routes, it will be send twice to the client. This is surely a limitation of the bundler/build system, in our case Next.js, and not related to the CSS-in-JS solution.

In Next.js, code-splitting works at the route level, bundling all components required for a specific route, but according to their official blog and web.dev if a component is used in more than 50% of the pages, it should be included in the commons bundle. However, in our example, we have 2 pages, each of them importing the Button component, and it's included in each page bundle, not in the commons bundle. Since the code required for styling is bundled with the component, this limitation will impact the styles as well, so it's worth keeping this in mind.

<br />
<br />

CSS Modules

This is a well established, mature and solid approach. Without a doubt, it's a great improvement over BEM, SMACCS, OOCSS, or any other scalable CSS methodology to structure and organize our CSS, especially in component-based applications.

Launched in 2015 | Back to Overview

<br /> <br />

This is the baseline we'll consider when comparing all the following CSS-in-JS solutions. Checkout the motivation to better understand the limitations of this approach that we're trying to fill.

<br />
Transferred / gzippedUncompressed
Index page size76.7 kB233 kB
<br />
Page                                Size     First Load JS
โ”Œ โ—‹ /                               2.19 kB        68.7 kB
โ”œ   โ”” css/1d1f8eb014b85b65feee.css  450 B
โ”œ   /_app                           0 B            66.5 kB
โ”œ โ—‹ /404                            194 B          66.7 kB
โ”” โ—‹ /other                          744 B          67.2 kB
    โ”” css/1c8bc5a96764df6b92b4.css  481 B
+ First Load JS shared by all       66.5 kB
  โ”œ chunks/framework.895f06.js      42 kB
  โ”œ chunks/main.b2b078.js           23.1 kB
  โ”œ chunks/pages/_app.40892d.js     555 B
  โ”œ chunks/webpack.ddd010.js        822 B
  โ”” css/a92bf2d3acbab964f6ac.css    319 B
<br/>
<br/>

Styled JSX

Very simple solution, doesn't have a dedicated website for documentation, everything is on Github. It's not popular, but it is the built-in solution in Next.js.

Version: 4.0 | Maintained by Vercel | Launched in 2017 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Overall, we felt like writting plain CSS, with the added benefit of being able to define the styles along with the component, so we don't need an additional .css file. Indeed, this is the philosophy of the library: supporting CSS syntax inside the component file. We can use any JS/TS constants of functions with string interpolation. Working with dynamic styles is pretty easy because it's plain JavaScript in the end. We get all these benefits at a very low price, with a pretty small bundle overhead.

The downsides are the overall experience of writting plain CSS. Without nesting support pseudo classes/elements and media queries getting pretty cumbersome to manage.

<br />
Transferred / gzippedUncompressed
Index page size79.5 kB245 kB
vs. CSS Modules+2.8 kB+12 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.65 kB        72.6 kB
โ”œ   /_app                        0 B              70 kB
โ”œ โ—‹ /404                         194 B          70.2 kB
โ”” โ—‹ /other                       1.18 kB        71.2 kB
+ First Load JS shared by all    70 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.a4b061.js  4.12 kB
  โ”” chunks/webpack.61f1b6.js     778 B
<br/>
<br/>

Styled Components

For sure one of the most popular and mature solutions, with good documentation. It uses Tagged Templates to defines styles by default, but can use objects as well. It also popularized the styled components approach, which creates a new component along with the defined styles.

Version: 5.3 | Maintained by Max Stoiber & others | Launched in 2016 | View Docs | ...ย backย toย Overview

<br /> <br />

Worth mentioning observations

<br />

Conclusions

Styled components offers a novel approach to styling components using the styled method which creates a new component including the defined styles. We don't feel like writting CSS, so coming from CSS Modules we'll have to learn a new, more programatic way, to define styles. Because it allows both string and object syntax, it's a pretty flexibile solution both for migrating our existing styles, and for starting a project from scratch. Also, the maintainers did a pretty good job keeping up with most of the innovations in this field.

However before adopting it, we must be aware that it comes with a certain cost for our bundle size.

<br />
Transferred / gzippedUncompressed
Index page size90.1 kB272 kB
vs. CSS Modules+13.4 kB+39 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.52 kB        83.1 kB
โ”œ   /_app                        0 B            80.6 kB
โ”œ โ—‹ /404                         194 B          80.8 kB
โ”” โ—‹ /other                       1.06 kB        81.7 kB
+ First Load JS shared by all    80.6 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.731ace.js  14.7 kB
  โ”” chunks/webpack.ddd010.js     822 B
<br/>
<br/>

Emotion

Probably the most comprehensive, complete and sofisticated solution. Detailed documentation, fully built with TypeScript, looks very mature, rich in features and well maintained.

Version: 11.4 | Maintained by Mitchell Hamilton & others | Launched in 2017 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Overall Emotion looks to be a very solid and flexible approach. The novel css prop approach offers great ergonomics for developers. Working with dynamic styles and TypeScript is pretty easy and intuitive. Supporting both strings and objects when defining styles, it can be easily used both when migrating from plain CSS, or starting from scratch. The bundle overhead is not negligible, but definitely much smaller than other solutions, especially if you consider the rich set of features that it offers.

It seems it doesn't have a dedicated focus on performance, but more on Developer eXperience. It looks like a perfect "well-rounded" solution.

<br />
Transferred / gzippedUncompressed
Index page size83.2 kB253 kB
vs. CSS Modules+6.5 kB+20 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.5 kB         76.4 kB
โ”œ   /_app                        0 B            73.9 kB
โ”œ โ—‹ /404                         194 B          74.1 kB
โ”” โ—‹ /other                       1.07 kB        74.9 kB
+ First Load JS shared by all    73.9 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.6cb893.js        23.3 kB
  โ”œ chunks/pages/_app.b6d380.js  7.68 kB
  โ”” chunks/webpack.ddd010.js     822 B
<br/>
<br/>

TypeStyle

Minimal library, focused only on type-checking. It is framework agnostic, that's why it doesn't have a special API for handling dynamic styles. There are React wrappers available, but the typings feels a bit convoluted.

Version: 2.1 | Maintained by Basarat | Launched in 2017 | View Docs | ...ย backย toย Overview

<br /> <br />

Worth mentioning observations

<br />

Conclusions

Overall TypeStyle seems a minimal library, relatively easy to adopt because we don't have to rewrite our components, thanks to the classic className approach. However we do have to rewrite our styles, because of the Style Object syntax. We didn't feel like writting CSS, so there is a learning curve we need to climb.

With Next.js or React in general we don't get much value out-of-the-box, so we still need to perform a lot of manual work. The external react-typestyle binding doesn't support hooks, it seems to be an abandoned project and the typings are too convoluted to be considered an elegant solution.

<br />
Transferred / gzippedUncompressed
Index page size78.8 kB241 kB
vs. CSS Modules+2.1 kB+8 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.44 kB        72.1 kB
โ”œ   /_app                        0 B            69.7 kB
โ”œ โ—‹ /404                         194 B          69.9 kB
โ”” โ—‹ /other                       975 B          70.7 kB
+ First Load JS shared by all    69.7 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.5b0422.js  3.81 kB
  โ”” chunks/webpack.61f1b6.js     778 B
<br/>
<br/>

Fela

It appears to be a mature solution, with quite a number of users. The API is intuitive and very easy to use, great integration for React using hooks.

Version: 11.6 | Maintained by Robin Weser | Launched in 2016 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Fela looks to be a mature solution, with active development. It introduces 2 great features which we enjoyed a lot. The first one is the basic principle that "Style as a Function of State" which makes working with dynamic styles feel super natural and integrates perfectly with React's mindset. The second is atomic CSS class names, which should potentially scale great when used in large applications.

The lack of TS support however is a bummer, considering we're looking for a fully type-safe solution. Also, the scaling benefits of atomic CSS should be measured against the library bundle size.

<br />
Transferred / gzippedUncompressed
Index page size88.6 kB276 kB
vs. CSS Modules+11.9 kB+43 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.84 kB        81.7 kB
โ”œ   /_app                        0 B            78.9 kB
โ”œ โ—‹ /404                         194 B            79 kB
โ”” โ—‹ /other                       1.43 kB        80.3 kB
+ First Load JS shared by all    78.9 kB
  โ”œ chunks/framework.2191d1.js   42.4 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.32bc1d.js  12.6 kB
  โ”” chunks/webpack.ddd010.js     822 B
<br/>
<br/>

Stitches

Very young library, solid, modern and well-thought-out solution. The overall experience is just great, full TS support, a lot of other useful features baked in the lib.

Version: 0.2.5 (beta) | Maintained by Modulz | Launched in 2020 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Stitches is probably the most modern solution to this date, with full out-of-the-box support for TS. Without a doubt, they took some of the best features from other solutions and put them together for an awesome development experience. The first thing that impressed us was definitely the documentation. The second, is the API they expose which is close to top-notch. The features they provide are not huge in quantity, but are very well-thought-out.

However, we cannot ignore the fact that it's still in beta. Also, the authors identify it as "near-zero runtime", but at +9 kB gzipped it's debatable.

<br />
Transferred / gzippedUncompressed
Index page size82.0 kB250 kB
vs. CSS Modules+5.3 kB+17 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.43 kB        75.2 kB
โ”œ   /_app                        0 B            72.8 kB
โ”œ โ—‹ /404                         194 B            73 kB
โ”” โ—‹ /other                       984 B          73.8 kB
+ First Load JS shared by all    72.8 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.ff82f0.js  6.93 kB
  โ”” chunks/webpack.61f1b6.js     778 B
<br/>
<br/>

JSS

Probably the grandaddy around here, JSS is a very mature solution being the first of them, and still being maintained. The API is intuitive and very easy to use, great integration for React using hooks.

Version: 10.7 | Maintained by Oleg Isonen and others | Launched in 2014 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

The API is similar in many ways to React Native StyleSheets, while the hooks helper allows for easy dynamic styles definition. There are many plugins that can add a lot of features to the core functionality, but attention must be payed to the total bundle size, which is significant even with the bare minimum only.

Also, being the first CSS-in-JS solution built, it lacks many of the modern features that focuses on developer experience.

<br />
Transferred / gzippedUncompressed
Index page size94.9 kB293 kB
vs. CSS Modules+18.2 kB+60 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.45 kB          88 kB
โ”œ   /_app                        0 B            85.6 kB
โ”œ โ—‹ /404                         194 B          85.8 kB
โ”” โ—‹ /other                       992 B          86.6 kB
+ First Load JS shared by all    85.6 kB
  โ”œ chunks/framework.2191d1.js   42.4 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.5f0007.js  19.2 kB
  โ”” chunks/webpack.9c89cc.js     956 B
<br/>
<br/>

Goober

A very light-weight solution, with a loads of features.

Version: 2.0 | Maintained by Cristian Bote | Launched in 2019 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Looking at Goober you cannot ask yourself what kind of magic did Cristian Bote do to fit all the features inside this tiny library. It is really mind blowing. It is marketed as being "less than 1KB", which is not entirely accurate, but still... it's the smallest library we've tested.

<br />
Transferred / gzippedUncompressed
Index page size77.8 kB237 kB
vs. CSS Modules+1.1 kB+4 kB
<br />
Page                             Size     First Load JS
โ”Œ โ—‹ /                            2.77 kB        71.1 kB
โ”œ   /_app                        0 B            68.3 kB
โ”œ โ—‹ /404                         194 B          68.5 kB
โ”” โ—‹ /other                       2.39 kB        70.7 kB
+ First Load JS shared by all    68.3 kB
  โ”œ chunks/framework.895f06.js   42 kB
  โ”œ chunks/main.b2b078.js        23.1 kB
  โ”œ chunks/pages/_app.5ee014.js  2.42 kB
  โ”” chunks/webpack.61f1b6.js     778 B
<br/>
<br/>

Compiled

A rather new library, having the huge Atlassian platform supporting and probably using it. Many existing features, even more in development, or planned for development.

Version: 0.6 | Maintained by Atlassian | Launched in 2020 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Compiled is a very promising library. Considering that it offers both atomic CSS, and it plans to support static .css extraction, with excellent TypeScript support and style co-location, it would be quite unique (having only style9 as a direct competitor).

Also, we cannot ignore that is has Atlassian supporting its development, which puts a (slightly) bigger weight on the confidence level.

The total bundle overhead is pretty small, the runtime library being quite light-weight. With static .css file extraction, this could potentially become even smaller.

<br />
Transferred / gzippedUncompressed
Index page size80.2 kB242 kB
vs. CSS Modules+3.5 kB+9 kB
<br />
Page                              Size     First Load JS
โ”Œ โ—‹ /                             2.11 kB        71.8 kB
โ”œ   /_app                         0 B            66.5 kB
โ”œ โ—‹ /404                          194 B          66.7 kB
โ”” โ—‹ /other                        888 B          70.6 kB
+ First Load JS shared by all     66.5 kB
  โ”œ chunks/framework.895f06.js    42 kB
  โ”œ chunks/main.b2b078.js         23.1 kB
  โ”œ chunks/pages/_app.ebe095.js   576 B
  โ”œ chunks/webpack.ddd010.js      822 B
  โ”” css/a92bf2d3acbab964f6ac.css  319 B

<br/>

Linaria

Linaria is all about static CSS extraction and avoiding any runtime overhead.

Version: 3.0 (beta) | Maintained by Callstack | Launched in 2018 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

Linaria is highly inspired from Astroturf, combining various features from other libraries.

Version 3 is currently in Beta, not sure what the changelog is compared to v2. It's still in development by the React/Native geeks at Callstack.io, but we couldn't find which of the big players use it in production.

It seems to have a slightly larger overall page size (2.9 KB), but we didn't investigate where does this come from. Also, there's an open question if this overhead is fixed or if it scales.

PS: thanks to Daniil Petrov for his PR with the Next.js integration

<br />
Transferred / gzippedUncompressed
Index page size79.4 kB239 kB
vs. CSS Modules+2.7 kB+6 kB
<br />
Page                                Size     First Load JS
โ”Œ โ—‹ /                               4.99 kB        71.5 kB
โ”œ   โ”” css/16f3e95ede28dcc048f2.css  423 B
โ”œ   /_app                           0 B            66.5 kB
โ”œ โ—‹ /404                            194 B          66.7 kB
โ”” โ—‹ /other                          3.59 kB        70.1 kB
    โ”” css/3064299bff08067ec7dd.css  427 B
+ First Load JS shared by all       66.5 kB
  โ”œ chunks/framework.895f06.js      42 kB
  โ”œ chunks/main.b2b078.js           23.1 kB
  โ”œ chunks/pages/_app.98e8c3.js     598 B
  โ”œ chunks/webpack.ddd010.js        822 B
  โ”” css/7739287c04a618ea0c54.css    295 B
<br/>
<br/>

vanilla-extract

Modern solution with great TypeScript integration and no runtime overhead. It's pretty minimal in its features, straightforward and opinionated. Everything is processed at compile time, and it generates static CSS files. Successor of Treat, also be called "Treat v3", is developed and maintained by the same authors.

Version: 1.2 | Maintained by Seek OSS | Launched in 2021 | View Docs | ...ย backย toย Overview

<br /> <br />

Other benefits

<br />

Worth mentioning observations

<br />

Conclusions

We felt a lot like using CSS Modules: we need an external file for styles, we place the styles on the elements using className, we handle dynamic styles with inline styles, etc. However, we don't write CSS, and the overall experience with TypeScript support is magnificent, because everything is typed, so we don't do any copy-paste. Error messages are very helpful in guiding us when we do something we're not supposed to do.

vanilla-extract is built with restrictions in mind, with a strong user-centric focus, balacing the developer experience with solid TypeScript support. It's also worth mentioning that Mark Dalgleish, co-author of CSS Modules, works at Seek and he's also a contributor.

The authors vision is to think of vanilla-extract as a low-level utility for building higher-level frameworks, which will probably happen in the future.

<br />
Transferred / gzippedUncompressed
Index page size76.7 kB231 kB
vs. CSS Modules+0.0 kB-2 kB
<br />
Page                                Size     First Load JS
โ”Œ โ—‹ /                               2.09 kB        68.5 kB
โ”œ   โ”” css/37c023369f5e1762e423.css  370 B
โ”œ   /_app                           0 B            66.4 kB
โ”œ โ—‹ /404                            194 B          66.6 kB
โ”” โ—‹ /other                          611 B            67 kB
    โ”” css/a56b9d05c6da35ff125f.css  386 B
+ First Load JS shared by all       66.4 kB
  โ”œ chunks/framework.895f06.js      42 kB
  โ”œ chunks/main.700159.js           23.1 kB
  โ”œ chunks/pages/_app.bfd136.js     565 B
  โ”œ chunks/webpack.61f1b6.js        778 B
  โ”” css/23b89d9ef0ca05e4b917.css    286 B
<br />

Libraries not included

We know there are a lot of other libraries out there, besides the ones covered above. We're only covered the ones that have support for React, support for SSR, an easy integration with Next.js, good documentation and a sense of ongoing support and maintenance. Please checkout our goals.

<br />

Treat

Treat was initially included in the analysis with v1.6, but removed for a few reasons:

  1. the library itself is replaced by vanilla-extract
  2. Next.js integration is not supported with v2
  3. we couldn't upgrade to Next.js v11/webpack 5 even with v1

The main difference between vanilla-extract and Treat is that the latter supports IE and legacy browsers as well.

<br />

style9

Style9 is a new library, inspired by Facebook's own CSS-in-JS solution called stylex. Style9 is unique because it's the only open source library that supports both .css static extraction + atomic CSS, and/or styles co-location. It has TS support and easy to integrate with Next.js.

However, it has quite a few limitations (at least as of Feb 2021) that makes it practically unusable in a real production application that we would want to scale, both in code & team size:

Some upsides:

As a conclusion, it wants to be a powerful solution with very interesting and unique set of features, but it's not mature yet. As far as we see, it's currently mostly designed towards more static solutions. Dynamic styling seems to be difficult to handle, at least for the moment.

<br />

Tailwind

Not an actual CSS-in-JS library, more like a replacement for traditional CSS styling. It uses atomic CSS classes (some of them having multiple properties) that we attach to html elements. We don't write CSS, instead we use a different DSL to specify styles, pseudo classes, media queries, etc.

The reason we didn't include it in our thorough review is because it doesn't fully meet our goals:

Some upsides:

Tailwind seems to be more than a styling tool, it also offers some out-of-the-box utils + a ready-made design system that you can use right away.

<br />

Aphrodite

It's not a popular solution, the approach is similar to React Native StyleSheets way of styling components. Has built-in TypeScript support and a simple API.

<br />

Glamor

I got it started with Next.js, but it feels fragile. The Glamor official example throws an error regarding rehydrate. When commenting it out, it works, but not sure what the consequences are.

<br />

Cxs

Didn't manage to start it with Next.js + TypeScript. The official example uses version 3, while today we have version 6. The example doesn't work, because the API has changed.

The solution looked interesting, because it is supposed to be very light-weight.

<br />

Astroturf

Didn't manage to start it with Next.js + TypeScript. There was an official example that used an older version of Next.js, but the example if not there anymore.

The solution is not that popular, but it was the first to use .css extraction with collocated styles.

<br />

Otion

Looks promising, atomic css and light-weight. It has a working Next.js example, but we didn't consider it because it lacks any documentation.

<br />

Styletron

It looks like a not so popular solution, which also lacks support for TypeScript. It looks like the maintainers work at Uber and they use it internally. It focused on generating unique atomic CSS classes, which could potentially deduplicate a lot of code.

<br />

Radium

The project was put in Maintenance Mode. They recommend other solutions.

<br />

Glamorous

The project was discontinued in favor of Emotion.

<br />

Running the examples

Each implementation sits on their own branch, so we can have a clear separation at built time.

# install dependencies
yarn

# for development
yarn dev

# for production
yarn build
yarn start
<br />

Feedback and Suggestions

To get in touch, my DMs are open @pfeiffer_andrei.

<br />

Special thanks and appreciations go to everyone that helped putting this document together, and making it more accurate: