What Actually Separates CSS-in-JS Libraries?
Choosing a CSS-in-JS library is often harder than picking a framework. More than 50 options exist, each advertising a distinct feature set. We tested 10 of them — Styled JSX, styled-components, Emotion, Treat, TypeStyle, Fela, Stitches, JSS, Goober, and Compiled — and found that while every library does have its own personality, most of the features that get the most attention are actually shared across the board.
So rather than walking through each library one by one, it makes more sense to look at where these libraries genuinely diverge. That helps clarify which tool fits which project, instead of getting lost in marketing bullet points.
The Baseline: Features Everyone Has
Nearly every actively maintained CSS-in-JS library ships with the same core set of features. You should treat these as table stakes, not differentiators.
Scoped Styles
Every library in this space generates unique class names, a technique pioneered by CSS Modules. This gives you component-level encapsulation without collisions, specificity fights, or the chore of inventing globally unique class names. For component-based development, this is invaluable.
Server-Side Rendering
For any site that needs to be indexed by search engines — or that uses a static site generator — styles must be generated on the server, not just in the browser. All 10 libraries we tested support SSR, which means none of them will rule out an SSR-based project.
Automatic Vendor Prefixing
Vendor prefixes are a necessary evil for supporting older browsers, and no one wants to write them by hand. Every library in our analysis generates the required prefixed properties automatically from standard CSS syntax, out of the box.
No Inline Styles
Older libraries like Radium and Glamor pushed styles into inline style attributes, which forced them to hack around missing support for pseudo-classes, pseudo-elements, and media queries by attaching DOM event listeners. That approach was both less performant and widely discouraged. Every current library in this space uses class names instead, which means there's no limit on which CSS properties you can use — including pseudo-classes, media queries, and keyframe animations.
Where the Libraries Actually Diverge
Once you move past the common ground, the real technical trade-offs begin. These are the decisions that should actually inform your choice.
React-Only or Framework-Agnostic
Styled JSX, styled-components, and Stitches are built specifically for React. Emotion, Treat, TypeStyle, Fela, JSS, and Goober are framework-agnostic, so they'll work in vanilla JavaScript or with other frameworks. If you're not using React, the decision is basically made for you. If you are, you need to dig deeper into the other differentiating features.
Co-Locating Styles and Components
Most libraries let you define styles next to the component that uses them, which removes the constant file-switching between markup and style definitions. This is a genuine developer experience win during both initial development and maintenance. The only exception in our testing was Treat, which requires styles to live in a separate .treat.ts file, much like CSS Modules.
Two Syntax Options
There are two fundamentally different ways to write styles in JavaScript, and they each come with their own ergonomics and costs.
Tagged template literals let you write plain CSS as a string:
// consider "css" being the API of a generic CSS-in-JS library
const heading = css`
font-size: 2em;
color: ${myTheme.color};
`;
This approach keeps CSS properties in kebab-case, supports JavaScript interpolation, and makes migrating existing CSS files trivial — no rewriting required. On the downside, you need an editor plugin for syntax highlighting and code suggestions, and the string needs to be parsed and converted to JavaScript at some point, either at runtime or at build time, which adds a small overhead.
Object syntax defines 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,
});
Here, properties come in camelCase and string values need quotes. Syntax highlighting is free because it's just JavaScript, and code completion requires the library to ship CSS type definitions (most extend the popular csstype package). There's no parsing or conversion step because the styles are already valid JavaScript. The trade-off: migrating existing CSS requires a full rewrite into the new syntax.
| Library | Tagged template | Object styles |
|---|---|---|
| styled-components | ✅ | ✅ |
| Emotion | ✅ | ✅ |
| Goober | ✅ | ✅ |
| Compiled | ✅ | ✅ |
| Fela | 🟠 | ✅ |
| JSS | 🟠 | ✅ |
| Treat | ❌ | ✅ |
| TypeStyle | ❌ | ✅ |
| Stitches | ❌ | ✅ |
| Styled JSX | ✅ | ❌ |
✅ Full support 🟠 Requires plugin ❌ Unsupported
How Styles Get Applied
Libraries differ in how you attach styles to your components.
The most traditional approach is to get a string of generated class names back from the library and attach it via a className prop:
// consider "css" being the API of a generic CSS-in-JS library
const heading_style = css({
color: "blue"
});
// Vanilla DOM usage
const heading = `<h1 class="${heading_style}">Title</h1>`;
// React-specific JSX usage
function Heading() {
return <h1 className={heading_style}>Title</h1>;
}
This closely resembles plain CSS — define styles, then attach them — so the learning curve is low for anyone who has written CSS before. Treat, TypeStyle, Fela, and Goober all work this way.
The styled component API — first popularized by styled-components — takes a different path. Instead of defining styles separately and mapping them to elements, you create a new component with styles baked in:
// consider "styled" being the API for a generic CSS-in-JS library
const Heading = styled("h1")({
color: "blue"
});
The API returns a component with class names already applied, which you can render anywhere. This removes the manual mapping step entirely. styled-components, Emotion, Stitches, and Goober all support this pattern.
A third approach, popularized by Emotion, is the css prop, which works only in JSX:
// React-specific JSX syntax
function Heading() {
return <h1 css={{ color: "blue" }}>Title</h1>;
}
This feels much like inline styles — no special API to import, just pass the styles directly. However, css isn't a standard HTML attribute, so it requires a Babel plugin from the library to function correctly.
| Library | className | <Styled /> | css prop |
|---|---|---|---|
| styled-components | ❌ | ✅ | ✅ |
| Emotion | ✅ | ✅ | ✅ |
| Goober | ✅ | ✅ | 🟠 2 |
| Compiled | 🟠 1 | ✅ | ✅ |
| Fela | ✅ | ❌ | ❌ |
| JSS | ✅ | 🟠 2 | ❌ |
| Treat | ✅ | ❌ | ❌ |
| TypeStyle | ✅ | ❌ | ❌ |
| Stitches | ✅ | ✅ | 🟠 1 |
| Styled JSX | ✅ | ❌ | ❌ |
✅ Full support 🟠 1 Limited support 🟠 2 Requires plugin ❌ Unsupported
Where the CSS Goes
The biggest architectural decision is whether styles get injected into the DOM at runtime or extracted to static files.
Runtime-Injected Styles
Most libraries inject a <style> tag (or use the CSSStyleSheet API) at runtime, with styles appended as a <style> tag in the <head> during SSR. This approach has several advantages:
- Inlining styles during SSR improves First Contentful Paint because rendering isn't blocked on a separate
.cssfile request. - Critical CSS extraction comes free — only the styles needed for the initial page render are inlined, and dynamic styles are left out.
- Dynamic styling is much easier to implement, which makes this approach well suited to highly interactive, client-side rendered single-page apps.
But there's a real cost in bundle size and network behavior:
- A runtime library must be shipped to handle dynamic styling in the browser.
- Inlined SSR styles aren't cached by default; they're part of the HTML file and go out on every request.
- During rehydration, the same styles are sent to the browser again as part of the JavaScript bundle, duplicating what's already in the HTML.
Static File Extraction
A much smaller set of libraries takes the opposite approach: generate static .css files at build time rather than injecting anything at runtime. This gives you the same loading profile as hand-written CSS:
- The total shipped code is smaller because there's no runtime library and no rehydration duplication.
- Static files benefit from browser caching for returning visitors.
- This is often the better fit for SSR pages and static site generation.
The drawbacks are equally clear:
- The very first page visit with an empty cache usually has a slower FCP because of the extra stylesheet request.
- Any dynamic styles get included in the pre-generated bundle, which can bloat the
.cssfile.
Treat was the only library we tested that does static extraction. Other tools like Astroturf, Linaria, and style9 support this too, but they weren't part of our hands-on analysis.
The Atomic CSS Angle
Some libraries have pushed optimization further with an approach inspired by utility-first frameworks like Tachyons and Tailwind. Instead of generating a class that bundles all the properties for an element, they generate one unique class per unique property/value pair:
/* classic, non-atomic CSS class */
._wqdGktJ {
color: blue;
display: block;
padding: 1em 2em;
}
/* atomic CSS classes */
._ktJqdG { color: blue; }
._garIHZ { display: block; }
/* short-hand properties are usually expanded */
._kZbibd { padding-right: 2em; }
._jcgYzk { padding-left: 2em; }
._ekAjen { padding-bottom: 1em; }
._ibmHGN { padding-top: 1em; }
This creates a high degree of reuse, since each atomic class can appear anywhere across the codebase. In large applications, the total number of distinct CSS properties tends to be finite, so the amount of CSS grows logarithmically rather than linearly as you add features.

The catch is that many class names need to be applied to each element, which inflates HTML file size slightly:
<!-- with classic, non-atomic CSS classes -->
<h1 class="_wqdGktJ">...</h1>
<!-- with atomic CSS classes -->
<h1 class="_ktJqdG _garIHZ _kZbibd _jcgYzk _ekAjen _ibmHGN">...</h1>
Essentially, you're moving some weight from CSS over to HTML. Whether the total bytes shipped actually drops depends heavily on the project, but generally it should — the reduction in CSS tends to outweigh the increase in HTML.
Emotion, Fela, Stitches, Goober, and Compiled all support atomic CSS to some degree, although Stitches calls it a "sprinkling" while Goober's implementation is a bit different.
Making the Choice
There's no objectively best library here. The right answer comes from your project's specific constraints. A few clarifying questions will get you most of the way:
- Are we using React? Non-React projects have to pick a framework-agnostic library; React projects have many more options to weigh.
- Is this a highly interactive, client-rendered app? If so, runtime-injected styles are fine, and the rehydration overhead or the lack of static
.cssextraction matters less. - Are we building an SSR site? Static
.cssextraction may be better so the styles can be cached by browsers. - Are we migrating existing CSS? Tagged template syntax lets you paste in existing style blocks without any rewriting.
- Do we care more about first-time users or returning visitors? Static files are best for repeat visits thanks to caching; runtime-injected styles get first-time users to content faster.
- Do we update styles frequently? Any cache on static files is wasted if you're constantly invalidating it.
- Do we reuse a lot of styles and components? Atomic CSS starts to shine at scale.
Answering those questions narrows the field considerably. After that, choosing between the final candidates becomes a matter of syntax preference and how much you value a particular library's runtime or build-time strategy.



