Last update: Jan 2021
This document contains a thorough analysis of all the current CSS-in-JS solutions. The baseline reference we'll use for comparison is a CSS Modules approach. We'll also use Next.js as a SSR framework for building resources. Last important aspect is type-safety with full TypeScript support.
Please checkout our goals before drawing your own conclusions.
1. Co‑location | 2. DX | 3. tag` ` |
4. { } |
5. TS | 6. & ctx |
7. Nesting | 8. Theme | 9. .css |
10. <style> |
11. Atomic | 12. className |
13. styled |
14. css prop |
15. Learn | 16. Lib (gzip/raw) | 17. Page (gzip/raw) | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CSS Modules | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | - | - | - |
Styled JSX | ✅ | 🟠 | ✅ | ❌ | 🟠 | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | 📉 | +3.9 kB / +10.7 kB |
+4.4 kB / TBD |
Styled Components | ✅ | 🟠 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | 🟠 | 📈 | +14.2 kB / +36.7 kB |
+14.5 kB / TBD |
Emotion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | 📉 | +7.5 kB / +19.0 kb |
+7.7 kB / TBD |
Treat | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | 📉 | - | - |
TypeStyle | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | 🟠 | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | 📈 | +3.1 kB / TBD |
+3.7 kB / TBD |
Fela | ✅ | 🟠 | 🟠 | ✅ | 🟠 | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | 📉 | +13.7 kB / TBD |
+13.7 kB / TBD |
Stitches | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | 📉 | +8.5 kB / TBD |
+9.0 kB / TBD |
JSS | ✅ | ❌ | ❌ | ✅ | ❌ | 🟠 | 🟠 | ✅ | ❌ | ✅ | ❌ | ✅ | 🟠 | ❌ | 📉 | +19.0 kB / TBD |
+20.0 kB / TBD |
LEGEND:
✅ - full & out-of-the-box support
🟠 - partial/limited support (or not ideal)
❌ - lack of support
- Co-location: ability to define styles within the same file as the component.
You can also extract the styles into a separate file and import them, but the other way around does not apply. - DX: Developer eXperience which includes:
- syntax highlighting
- code-completion for CSS properties and values
tag` `
: support for defining styles as strings- uses ES Tagged Templates and
kebab-case
for property names, just like plain CSS syntax - enables easier migration from plain CSS to CSS-in-JS, because you don't have to re-write your styles
- requires additional code editor plugins for syntax highlight and code completion
- usually implies slightly larger bundles and slower performance, because the strings must be parsed before
- uses ES Tagged Templates and
{ }
: support for defining styles as objects- uses plain JacaScript Objects and
camelCase
for property names - more suitable for new projects, when you don't need to migrate existing CSS
- without TS support, you won't get code completion
- uses plain JacaScript Objects and
- TS: TypeScript support for library API, either built-in, or via
@types
package, which should include- typings for the library API
- Style Object typings (in case the library supports the object syntax)
Props
generics (if needed)
&
ctx: support for contextual styles, allowing to easily define pseudo classes/elements and media queries without the need to repeat yourself- can either support the SASS/LESS/Stylus
&
parent selector - or provide some specific API or syntax to achieve this
- can either support the SASS/LESS/Stylus
- Nesting: support for arbitrary nested rules/selectors
- this feature allows for great flexibility, which is required in some specific use-cases
- but it also introduces too many ways of defining styles, which might cause chaos in very restrictive use-cases, or when you want to enforce good-practices, consistency, scalability and maintainability
- Theme: built-in support for Theming or managing design tokens/system
.css
: support for extracting and serving the styles as native.css
files- this increases FCP metric because the document is parsed faster, and .css files can be fetched in parallel with other resources
- it also reduces bundle size, because you don't need runtime styles evaluation, to inject the styles
- dynamic styling could potentially increase the generated file, because all style combinations must be pre-generated at built time
- more suitable for less dynamic solutions (ie: e-commerce)
style
tag: support for serving the styles as injected<style>
tags in the document's<head>
- makes dynamic styling super easy
- incurs longer load
- more suited for highly dynamic and interactive applications
- Atomic: ability to generate atomic css classes and increasing reusability, reducing style duplication
- this generates a separate CSS class for each CSS property
- you'll get larger HTML files, because each element will contain a large number of CSS classes applied
- theoretically atomic CSS-in-JS reduces the scaling factor of your styles, Facebook is doing it as well
className
: the API returns a string which you have to add to your component/element- similar how you would normally style React components, so it's easy to adopt because you don't have to learn a new approach
- you'll probably have to use string concatenation, or interpolation, to combine styles
styled
: the API creates a wrapper (styled) component which includes theclassName
(s)- you'll have to learn a new way to define styles
- it also introduces a bit of indiretion when figuring out what native element gets rendered
- first introduced and popularized by Styled Components
css
prop: allows passing styles using a special css prop, similar to inline styles- this is usually an additional feature for styled components, but it can also work separately
- it's a nice and flexible ergonomic API
- first introduced and popularized by Emotion v10
- Learn: a slightly subjective opinion regarding the learning curve, you should really evaluate this on your own
- Lib: size in kB of the library that is shipped in a production build
- Bundle: the increase in kB compared to CSS Modules, for the entire index page production build
- keep in mind that this includes an almost empty page, with only a couple of components
- this is great for evaluating the minimal overhead, but does not offer any insight on the scaling factor: logarithmic, linear, or exponential
The following observations apply for all solutions (with minor pointed exceptions) and are explained here.
✅ 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.
✅ Global styles
All solutions offer a way to define global styles, some with a separate API.
- JSS has a convoluted API for this, which requires an additional plugin, which we didn't figure out how to implement
✅ SSR
All solutions are able to be Server-Side Rendered by Next.js.
✅ Vendor prefixes
All solutions add vendor specific prefixes out-of-the-box.
- JSS requires an additional plugin for this
✅ Unique class names
All solutions generate unique class names, like CSS Modules do.
✅ No inline styles
None of the solutions generate inline styles, which is an older approach, used by pioneers like Radium & Glamor. The approach is less performant than CSS classes, so it's not recommended. 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.
✅ Full CSS support
All solutions support most CSS properties that you would need: pseudo classes & elements, media queries, keyframes are the ones that we tested.
🟠 Increased FCP
For solutions that don't support .css
file extraction, SSRed styles are added as <style>
tags in the <head>
, which will result in higher FCP than using regular CSS, because .css
files can and will be loaded in paralel to other resources, while big <style>
content will be sent and parsed along with the HTML, increasing parsing time.
- solutions that perform
.css
file extraction don't have this problem (this includes CSS Modules and Treat)
🟠 Dead code removal
Most solution say they remove unused code/styles. This is only half-true. Unused code is indeed more difficult to accumulate, especially of you compare it to large .css
files as we used to write a century ago. But when compared to CSS Modules, the differencies are not that big. Any solution that offers the option to write selectors or nested styles will bundle unused styles. Even solutions that don't offer this option, have the same problem.
Basically, what you get is code removal when you delete the component, because the styles are colocated.
🟠 Debugging / Inspecting
Most solutions inject the <style>
tag in the DOM in DEVELOPMENT
, which is a slower approach, but enables style inspecting using browser dev tools. But when building for PRODUCTION
, they use CSSStyleSheet.insertRule()
to inject the styles directly into the CSSOM, which is a way faster approach, but you cannot inspect the styles.
- JSS and Stitches use
insertRule()
in dev mode as well, so you cannot see what gets injected - TypeStyle does NOT use
insertRule()
, not even in production
❌ 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.
This is a well established, mature and solid approach. Without a doubt, it's a great improvement over BEM, SMACCS, or any other methodology to structure and organize your CSS, especially in component-based applications.
-
✅ Context-aware code completion
-
❌ No Styles/Component co-location
-
❌ No TypeScript support
-
❌ No Atomic CSS
-
❌ No Theming support
-
Styles definition method(s)
- ✅ plain CSS
- ❌ Style Objects
-
Styles nesting
- ❌ Contextual styles: (requires SASS, LESS or Stylus)
- ✅ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ❌
styled
component - ❌
css
prop
- ✅
-
Styles output
- ✅
.css
file extraction - ❌
<style>
tag injection
- ✅
-
📉📈 Learning curve: easy to learn, but difficult to master
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.
Transferred / gzipped | Uncompressed | |
---|---|---|
Runtime library | - | - |
Index page size | 71.5 kB | 201 kB |
Page Size First Load JS
┌ ○ / 2.15 kB 64.9 kB
├ └ css/7a5b6d23ea12e90bddea.css 407 B
├ /_app 0 B 62.7 kB
├ ○ /404 3.03 kB 65.7 kB
└ ○ /other 706 B 63.4 kB
└ css/57bb8cd5308b249275fa.css 443 B
+ First Load JS shared by all 62.7 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.03531f.js 6.62 kB
├ chunks/pages/_app.6e472f.js 526 B
├ chunks/webpack.50bee0.js 751 B
└ css/d9aac052842a915b5cc7.css 325 B
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: 3.4
| Maintained by Vercel | Launched in 2017 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
🟠 Context-aware code completion: to get syntax highlighting & code completion, an editor extension is required
-
🟠 TypeScript support:
@types
can be additionaly installed, but the API is too minimal to require TS -
❌ No Atomic CSS
-
❌ No Theming support
-
Styles definition method(s)
- ✅ Tagged Templates
- ❌ Style Objects
-
Styles nesting
- ❌ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ❌
styled
component - ❌
css
prop
- ✅
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📉 Low Learning curve: because the API is minimal and very simple
- 😌 out-of-the-box support with Next.js
- 👍 for user input styles, it generates a new class name for each change, but it removes the old one
- 😏 unlike CSS modules, we can target HTML
elements
also, and it generates unique class names for them
- 🤓 we'll need to optimize our styles by splitting static & dynamic styles, to avoid rendering duplicated styles
- 🤨 unique class names are added to elements, even if we don't target them in our style definition, resulting in un-needed slight html pollution
- 😕 it will bundle any defined styles, regardless if they are used or not, just like plain CSS
- 😢 cannot use nesting, so defining pseudo classes or media queries has the same downsides as plain CSS, requiring selectors/class names duplication, so we might have to add SASS support to get this feature
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. We can also use any JS/TS constants of functions. 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.
Transferred / gzipped | Uncompressed | |
---|---|---|
Runtime library | 3.9 kB | 10.7 kb |
Index page size | 75.1 kB | 214 kB |
vs. CSS Modules | +3.6 kB | +13 kB |
Page Size First Load JS
┌ ○ / 2.64 kB 69.3 kB
├ /_app 0 B 66.6 kB
├ ○ /404 3.03 kB 69.6 kB
└ ○ /other 1.17 kB 67.8 kB
+ First Load JS shared by all 66.6 kB
├ chunks/1dfa07d0b4ad7868e7760ca51684adf89ad5b4e3.3baab1.js 3.53 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.99ad68.js 6.62 kB
├ chunks/pages/_app.949398.js 907 B
└ chunks/webpack.50bee0.js 751 B
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.2
| Maintained by Max Stoiber & others | Launched in 2016 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
✅ TypeScript support:
@types
must be additionaly installed, via DefinitelyTyped -
✅ Built-in Theming support
-
🟠 Context-aware code completion: requires an editor extension/plugin
-
❌ No Atomic CSS
-
Styles definition method(s)
- ✅ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ❌
className
- ✅
styled
component - 🟠
css
prop
- ❌
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📈 Higher Learning curve: we have to learn the API, get used to using the
styled
wrapper components, and basically get used to a new way to manage our styles
- 🧐 the
css
prop is mentioned in the API docs, but there are no usage examples - 🤓 we need to split static & dynamic styles, otherwise it will render duplicate output
- 😕 bundles nested styles even if they are not used in component
- 😵 we can mix Tagged Templates with Styled Objects, which could lead to convoluted and different syntax for each approach (kebab vs camel, EOL character, quotes, interpolation, etc)
- 🥴 some more complex syntax appears to be a bit cumbersome to get right (mixing animations with Styled Objects, dynamic styles based on
Props
variations, etc) - 🤫 for user input styles, it generates a new class name for each update, but it does NOT remove the old ones, appending indefinitely to the DOM
Styled components offers a novel approach to styling components using the styled
method which creates a new component including the defined styles. You 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.
Transferred / gzipped | Uncompressed | |
---|---|---|
Runtime library | 14.2 kB | 36.7 kb |
Index page size | 85.4 kB | 240 kB |
vs. CSS Modules | +13.9 kB | +39 kB |
Page Size First Load JS
┌ ○ / 2.5 kB 79.4 kB
├ /_app 0 B 76.9 kB
├ ○ /404 3.03 kB 79.9 kB
└ ○ /other 1.04 kB 77.9 kB
+ First Load JS shared by all 76.9 kB
├ chunks/1dfa07d0b4ad7868e7760ca51684adf89ad5b4e3.3f0ffd.js 13.8 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.99ad68.js 6.62 kB
├ chunks/pages/_app.7093f3.js 921 B
└ chunks/webpack.50bee0.js 751 B
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.1
| Maintained by Mitchell Hamilton & others | Launched in 2017 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
✅ TypeScript support
-
✅ Built-in Theming support
-
✅ Context-aware code completion: for using the
styled
components approach, an additional editor plugin is required -
❌ No Atomic CSS
-
Styles definition method(s)
- ✅ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ❌
className
- ✅
styled
component - ✅
css
prop
- ❌
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📉 Low Learning curve: when using the
css
prop, which is the primary approach, the API is pretty straightforward (thestyled
approach however incurs the same learning curve for Styled Components)
- 😎 the
css
prop offers great ergonomics during development, however it seems to be a newer approach, based on React 17 newjsx
transform, and configuring it is not trivial, differs on your setup, and implies some boilerplate (which should change soon and become easier)
- 😕 bundles nested styles even if they are not used in component
- 🤫 for user input styles, it generates a new class name for each update, but it does NOT remove the old ones, appending indefinitely to the DOM
- 😑 using
styled
approach will add3 kB
to our bundle, because it's imported from a separate package - 🤔 don't know how to split static and dynamic styles, resulting in highly polluted duplicated styles in head for component variants (same applies to
css
prop &styled
components)
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.
Transferred / gzipped | Uncompressed | |
---|---|---|
Runtime library | 7.5 kB | 19.0 kb |
Index page size | 78.4 kB | 221 kB |
vs. CSS Modules | +6.9 kB | +20 kB |
Page Size First Load JS
┌ ○ / 2.47 kB 72.6 kB
├ /_app 0 B 70.1 kB
├ ○ /404 3.03 kB 73.1 kB
└ ○ /other 1.04 kB 71.1 kB
+ First Load JS shared by all 70.1 kB
├ chunks/1dfa07d0b4ad7868e7760ca51684adf89ad5b4e3.19c2e4.js 7.1 kB
├ chunks/commons.800e6d.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.45755e.js 6.55 kB
├ chunks/pages/_app.398ef5.js 832 B
└ chunks/webpack.50bee0.js 751 B
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, similar to CSS Modules, Linaria, or Astroturf.
Version: 1.6
| Maintained by Seek OSS | Launched in 2019 | View Docs | ... back to Overview
-
✅ TypeScript support
-
✅ Built-in Theming support
-
✅ Context-aware code completion
-
❌ No Styles/Component co-location: styles must be placed in an external
.treat.ts
file -
❌ No Atomic CSS
-
Styles definition method(s)
- ❌ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ❌ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ❌
styled
component - ❌
css
prop
- ✅
-
Styles output
- ✅
.css
file extraction - ❌
<style>
tag injection
- ✅
-
📉 Low Learning curve: coming from CSS Modules it feels like home, the additional API required for variants is pretty straightforward and easy to learn
- 👮 forbids nested arbitrary selectors (ie:
& > span
), which might be seen as a downside, when it's actually discourages bad-practices like specificity wars
- 😕 bundles styles even if they are not used in component
- 😥 it doesn't handle dynamic styles: you can use built-in
variants
based on predefined types, or inline styles for user defined styles
When using Treat, 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. It's also the only analyzed solution the extracts styles as .css
files at built time, which should greatly improve the page load metrics.
The only thing to look out for is the limitation regarding dynamic styling. In highly interactive UIs that require user input styling, we'll have to use inline styles.
Treat 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.
Page overhead: -
Page Size First Load JS
┌ ○ / 2.11 kB 64.8 kB
├ └ css/4ca0d586ad5efcd1970b.css 422 B
├ /_app 0 B 62.7 kB
├ ○ /404 3.03 kB 65.8 kB
└ ○ /other 632 B 63.4 kB
└ css/adb81858cf67eabcd313.css 435 B
+ First Load JS shared by all 62.7 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.03531f.js 6.62 kB
├ chunks/pages/_app.2baddf.js 546 B
├ chunks/webpack.50bee0.js 751 B
└ css/08916f1dfb6533efc4a4.css 286 B
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
-
✅ Styles/Component co-location
-
✅ TypeScript support
-
✅ Context-aware code completion
-
🟠 Built-in Theming support: uses TS
namespaces
to define theming, which is not recommended even by the author himself, or by TS core team member Orta Therox. -
❌ No Atomic CSS
-
Styles definition method(s)
- ❌ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ❌
styled
component - ❌
css
prop
- ✅
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📈 High Learning curve: the API is simple, but it doesn't provide a lot of features, so we'll still need to do manual work and to re-adjust the way we'll author styles
- 😕 bundles nested styles even if they are not used in component
- 😕 it doesn't handle dynamic styles, so we have to use regular JS functions to compute styles
- 🤨 when composing styles, we'll have to manually add some internal typings
- 🤔 don't know how to split dynamic and static styles, so it's very easy to create duplicated generated code
- 😱 it creates a single
<style>
tag with all the styles, and replaces it on update, and apparently it doesn't useinsertRule()
, not even in production builds, which might be an important performance drawback in large & highly dynamic UIs
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.
Page overhead: +3.7 kB
Page Size First Load JS
┌ ○ / 2.41 kB 68.6 kB
├ /_app 0 B 66.2 kB
├ ○ /404 3.03 kB 69.2 kB
└ ○ /other 953 B 67.1 kB
+ First Load JS shared by all 66.2 kB
├ chunks/1dfa07d0b4ad7868e7760ca51684adf89ad5b4e3.250ad4.js 3.09 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.99ad68.js 6.62 kB
├ chunks/pages/_app.d59d73.js 893 B
└ chunks/webpack.50bee0.js 751 B
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.5
| Maintained by Robin Weser | Launched in 2016 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
✅ Built-in Theming support
-
✅ Atomic CSS
-
🟠 TypeScript support: it exposes Flow types, which work ok, from our (limited) experience
-
🟠 Context-aware code completion: styles defined outside the component require explicit typing to get code completion
-
Styles definition method(s)
- 🟠 Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ❌
styled
component - ❌
css
prop
- ✅
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📉 Low Learning curve: the API is simple, considering that we're comfortable with React hooks
- 😌 easy and simple to use API, very intuitive
- 🥳 creates very short and atomic class names (like
a
,b
, ...) - 😎 it has a lot of plugins that can add many additional features (but will also increase bundle size)
- 😕 bundles nested styles even if they are not used in component
- 🤨 when defining styles outside the component, we have to explicitly add some internal typings to get code completion
- 🥺 there's no actual TS support and the maintainer considers it a low priority
- 🤕 without TS support, we cannot get fully type-safe integration into Next.js + TS (there are missing types from the definition file)
- 🤔 the docs say it supports string based styles, but they are a second-class citizen and they seem to work only for global styles
- 😵 some information in the docs is spread on various pages, sometimes hard to find without a search feature, and the examples and use cases are not comprehensive
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.
Page overhead: +13.7 kB
Page Size First Load JS
┌ ○ / 3.46 kB 78.6 kB
├ /_app 0 B 75.2 kB
├ ○ /404 3.03 kB 78.2 kB
└ ○ /other 2.06 kB 77.2 kB
+ First Load JS shared by all 75.2 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.37f4a7.js 42.1 kB
├ chunks/main.03531f.js 6.62 kB
├ chunks/pages/_app.f7ff86.js 12.6 kB
└ chunks/webpack.50bee0.js 751 B
Very young library, probably the most 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.0.2
| Maintained by Modulz | Launched in 2020 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
✅ TypeScript support
-
✅ Context-aware code completion
-
✅ Built-in Theming support
-
✅ Atomic CSS
-
Styles definition method(s)
- ❌ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- ✅ Contextual styles
- ✅ Abitrary nesting
-
Styles apply method(s)
- ✅
className
- ✅
styled
component - ✅
css
prop (used only to overridestyled
components)
- ✅
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📉 Low Learning curve: the API is simple and intuitive, documentation is top-notch
- 😌 easy and simple to use API, a pleasure to work with
- 😎 great design tokens management and usage
- 🥰 documentation is exactly what we'd expect: no more, no less
- 😕 bundles nested styles even if they are not used in component
- 😵 uses
insertRule()
in development also, so we cannot see what gets bundled - 🤨 it expands short-hand properties, from
padding: 1em;
will becomepadding-top: 1em; padding-right: 1em; padding-bottom: 1em; padding-left: 1em;
- 🤔 dynamic styles can be defined either using built-in
variants
(for predefined styles), or styles created inside the component to get access to theprops
- 🧐 would help a lot to get the search feature inside the docs
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 "light-weight", but at 8 kB it's worth debating. Nevertheless, we will keep our eyes open and follow its growth.
Page overhead: +8.5 kB
Page Size First Load JS
┌ ○ / 2.42 kB 73.9 kB
├ /_app 0 B 71.5 kB
├ ○ /404 3.03 kB 74.5 kB
└ ○ /other 959 B 72.4 kB
+ First Load JS shared by all 71.5 kB
├ chunks/1dfa07d0b4ad7868e7760ca51684adf89ad5b4e3.f723af.js 8.46 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.9d5241.js 41.8 kB
├ chunks/main.99ad68.js 6.62 kB
├ chunks/pages/_app.51b7a9.js 832 B
└ chunks/webpack.50bee0.js 751 B
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.5
| Maintained by Oleg Isonen and others | Launched in 2016 | View Docs | ... back to Overview
-
✅ Styles/Component co-location
-
✅ Built-in Theming support
-
❌ Atomic CSS
-
❌ TypeScript support
-
❌ Context-aware code completion
-
Styles definition method(s)
- ❌ Tagged Templates
- ✅ Style Objects
-
Styles nesting
- 🟠 Contextual styles: (works for pseudo classes/elements, not for media queries)
- 🟠 Abitrary nesting: (requires separate plugin)
-
Styles apply method(s)
- ✅
className
- 🟠
styled
component (see details below) - ❌
css
prop
- ✅
-
Styles output
- ❌
.css
file extraction - ✅
<style>
tag injection
- ❌
-
📉 Low Learning curve: the API is simple, considering that we're comfortable with React hooks
- 😌 easy and simple to use API, very intuitive
- 😎 it has a lot of plugins that can add many additional features (but will also increase bundle size)
- 😕 bundles nested styles even if they are not used in component
- 🤔
react-jss
uses className by default. There's alsostyled-jss
that uses Styled Components approach, but it has no types, and couldn't make it work on top ofreact-jss
. - 😖 global styles are cumbersome to setup, requires plugin, tried to mix the JSS setup docs, with the
react-jss
SSR setup docs, with theplugin-globals
docs on usage, without any luck
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.
Page overhead: +20.0 kB
Page Size First Load JS
┌ ○ / 1.98 kB 84.9 kB
├ /_app 0 B 64.3 kB
├ ○ /404 3.03 kB 67.3 kB
└ ○ /other 501 B 83.5 kB
+ First Load JS shared by all 64.3 kB
├ chunks/commons.7af247.js 13.1 kB
├ chunks/framework.37f4a7.js 42.1 kB
├ chunks/main.99ad68.js 6.62 kB
├ chunks/pages/_app.ea9fff.js 1.78 kB
├ chunks/webpack.50bee0.js 751 B
└ css/d9aac052842a915b5cc7.css 325 B
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.
- global styles are a bit cumbersome to define
- able to nest media queries & pseudo selectors, but cannot nest arbitrary rules/selectors
- no dynamic out-of-the-box support, so you have to get around that, like inline styles I guess, or like in React Native
- doesn't add any real value, except the ergonomics to colocate styles with the component.
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.
- it looks like an unmaintained or abandoned package
- documentation is pretty minimal
- lacks any TS support
- has a lot of documented experimental features, marked as "buggy"
- it feels like a side/internal project at FB, that is not used anymore.
Didn't manage to start it with Next.js + TypeScript.
It was an interesting solution, as it promises zero-runtime overhead, generating .css
files at build time, while the style are colocated within the components.
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.
Didn't manage to start it with Next.js + TypeScript. The official example uses an older version of Next.js.
The solution is not that popular, but it was the first to use .css
extraction with colocated styles.
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.
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.
The projest was put in Maintenance Mode. They recommend other solutions.
The project was discountinued in favor of Emotion.
The CSS language and CSS Modules approach have some limitations especially if you want to have solid and type-safe code. Some of these limitations have altenative solutions, others are just being "annoying" and "less ideal":
-
Styles cannot be co-located with components
This can be frustrating when authoring many small components, but it's not a deal breaker. For large components/containers/pages/screens this isn't an actual issue, because you probably prefer to extract the styles in a separate file. -
Styling pseudos and media queries requires duplication
Another frustrating fact at some point is the need to duplicate your class name when defining pseudo classes and elements, or media queries. You can overcome these limitations using a CSS preprocessor like SASS, LESS or Stylus, which all support the&
parent selector, enabling contextual styling. -
Styles usage is disconnected from their definition
You get no IntelliSense with CSS Modules, of what styles/classes are defined in the.module.css
files, making copy-paste a required tool, lowering the DX. It also makes refactoring very cumbersome, because of the lack of safety. -
Styles cannot access design tokens
Any design tokens, defined in JS/TS cannot be directly used in CSS. There are 2 workarouns for this issue, neither of them being elegant:- We could inject them as CSS Variables, but we still don't get any IntelliSense or type-safety
- We could use inline styles, which is less performant and also introduces another way to write styles (camelCase vs. kebab-case), while also splitting the styling in 2 different places.
There are specific goals we're looking for:
- 🥇 SSR support and easy integration with Next.js
- 🥇 full TypeScript support
- 🥇 great DX with code completion & syntax highlight
- 🥈 light-weight
- 🥉 low learning curve and intuitive API
Getting even more specific, we wanted to experience the usage of various CSS-in-JS solutions regarding:
- defining global styles
- using media queries & pseudo classes
- dynamic styles based on component
props
(aka. component variants), or from user input - bundle size impact
This analysis is intended to be objective and unopinionated:
- I don't work on any of these solutions, and have no intention or motivation of promoting or trashing either of them.
- I have no prior experience with any CSS-in-JS solution, so I'm not biased towards any of them. I've equally used all the solutions analyzed here.
👎 What you WON'T FIND here?
- which solution is "the best", or "the fastest", as I'll not add any subjective grading, or performance metrics
- what solution should you pick for your next project, because I have no idea what your project is and what your goals are
👍 What you WILL FIND here?
- an overview of (almost) all CSS-in-JS solutions available at this date (see last update on top) that we've tried to integrate into a Next.js v10 + TypeScript empty project, with minimal effort;
- a limited set of quantitative metrics that allowed me to evaluate these solutions, which might help you as well;
- an additional list of qualitative personal observations, which might be either minor details or deal-breakers when choosing a particular solution.
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 talk by Max Stoiber.
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
To get in touch, my DMs are open @pfeiffer_andrei