Ah, styled-components
, what a journey it has been.
This little package took the npm downloads by storm, releasing back in 2017, promising to be the messiah that will relinquish us from monsters such as class name collisions and globally-scoped styles.
It ushered in the age of CSS-in-JS packages, and before long, writing CSS in your JavaScript was all the rage and styled.div
was the new black.
However, fast-forward to 2024, and people seem to be hating and moving away from styled-components
, in favor of competitor packages that are more performant and type-safe.
Packages like TailwindCSS
, Linaria
, Vanilla Extract
, PandaCSS
, and more are getting ever so popular, while styled-components
is slowly becoming the Facebook of styling libraries — still there, but everyone’s quietly leaving.
I still love styled-components
.
It’s been my go-to CSS solution for years.
Their React-friendly approach of using components for styling has felt very fitting in a framework that relies on JSX, and I liked how, for the most part, you were still writing actual CSS syntax, instead of an object containing a camel-cased representation of the same CSS attributes.
On top of that, its ability to dynamically inject styles based on props was extraordinarily comfortable in many situations.
However, it might be time for a change.
Even I can’t ignore some glaring issues this package has.
So, in this piece, I thought I would dive into how other modern packages compare to good old styled-components
.
With Great Power Comes a Great Performance Hit
One of the key selling points ofstyled-components
is its dynamic nature, but this has always been both its superpower and its biggest drawback.The JS runtime that dynamically generates styles and injects them into the DOM every time a component renders can become a performance bottleneck as your app scales.
It was revolutionary once, but now, it’s like asking a carpenter to build your furniture from scratch every time you walk into your living room instead of just using what’s already there.
Let’s take an example:
const Button = styled.button`
background: green;
color: white;
`;
Every time this button is rendered, styled-components
injects the CSS into the
of your document, potentially bloating your JavaScript bundle and slowing down load times as your app grows.<head>
In comparison:
TailwindCSS
generates all CSS classes ahead of time. You simply reference these pre-generated classes in your HTML, leading to much faster rendering with no runtime CSS injection.Linaria
is another CSS-in-JS solution, but it’s zero-runtime, meaning it extracts the CSS at build time. This approach is similar to howVanilla Extract
works, providing a better performance profile.CSS Modules
also pre-compiles CSS. The class names are scoped locally, but all styles are generated and ready at build time.
Nesting ThemeProviders Like There’s No Tomorrow
styled-components
allows you to define themes and pass them down through a <ThemeProvider>
.This might seem convenient at first, but it can get cumbersome quickly.
You’re nesting your entire application inside a provider just to get consistent theming.
While this is something that I usually wouldn’t consider a big issue, we’ve has some bad experience with this as the app we were working on grew larger and more complex.
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#0070f3',
secondary: '#ff4081'
},
};
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
What’s happening behind the scenes: styled-components
uses React’s context API to inject the theme into every styled component.
At runtime, it calculates the styles based on the theme and injects them into the page. This adds overhead, especially in larger apps.
In comparison:
TailwindCSS
doesn’t require any providers or runtime calculations. You define your design system in a configuration file (tailwind.config.js), and Tailwind generates all the necessary CSS at build time. This results in a much smaller runtime overhead.Vanilla Extract
also generates CSS at build time, ensuring no runtime overhead. You define themes using TypeScript, and the styles are statically extracted.PandaCSS
, Similar to Vanilla Extract, focuses on design tokens and generates CSS at build time, avoiding runtime performance hits.
Typo-Safe, Not Type-Safe
styled-components
is dynamic, and with dynamism comes a lack of type safety.Sure, you can integrate TypeScript, but it’s not inherently type-safe like
Vanilla Extract
or CSS Modules
.This can lead to bugs and styling issues that only surface at runtime.
For example:
const Title = styled.h1`
font-size: props => props.size;
`;
If props.size
is undefined, you’ll only find out that something is wrong when the component is rendered, leading your users to question if your site’s text styling is a postmodern fashion statement.
In comparison:
Linaria
lets you define styles statically, which means TypeScript checks your code before it runs. By using design tokens or variables, Linaria enforces more predictable, type-safe styling.PandaCSS
takes a similar token-based approach to styling, focusing on type safety and design tokens.TailwindCSS
itself doesn’t rely on TypeScript directly, but its static configuration file and predefined classes guarantee consistent and reliable styles.
Bundle Bloat: Because More is Clearly More
One of the biggest complaints aboutstyled-components
is that it can bloat your JavaScript bundles.Every styled component adds to the overall weight of your bundle, making it harder to achieve fast-loading apps.
The tighter coupling of JavaScript and CSS means your bundle gets bigger with every new styled component.
If you have 100 different styled components, that’s a lot of JavaScript being generated just to manage styles.
In comparison:
TailwindCSS
generates all styles ahead of time, so the only thing you’re loading in your bundle are the necessary utility classes. You don’t have to worry about CSS being tightly coupled with your JS, resulting in much leaner bundles.Linaria
&PandaCSS
generate CSS at build time, meaning there’s no runtime overhead. The styles are completely separated from your JavaScript, keeping bundle sizes down.CSS Modules
are also precompiled at build time, and since styles are scoped locally to each component, you’re not dealing with large, global stylesheets or unnecessary CSS being bundled with your JS.
Server Components: It All Depends On the Context
Due tostyled-components
’s heavy dependency on React’s Context API, it’s impossible to use it with React Server Components, as that API simply doesn’t exist in a server-rendered component.In general, the React team hasn’t yet offered an RSC-compatible alternative to Context, leaving
styled-components
with no way to share data like the theme values between components to ensure styles are applied correctly.The same applies to
Emotion
, which is very similar to styled-components
.A quick look at the source code will show you that styled-components
clings to the Context API like it’s on life support—take it away, and the whole thing flatlines.
It’s therefore very baffling to understand how the maintainers will work their way out of this issue, as RSC could likely become a much more popular pardigm in the coming years.
It almost seems like they either need to wait for the React team to bail them out by releasing some magical new API, or simply rewrite the entire package and release a new version with so many breaking changes, you’d have to hold a support group just to cope with the migration.
In comparison:
Linaria
is great for RSC since it doesn’t rely on client-side JavaScript for styles, ensuring fast server-side rendering.CSS Modules
&TailwindCSS
generates static styles that are resolved at build time, making it suitable for server-side rendering.PandaCSS
was designed to be RSC-friendly, generating static styles without a runtime.
While competitor packages may address many of the drawbacks of styled-components
, none of them are flawless, and each has its own challenges.
Let’s take a closer look at where they fall short:
TailwindCSS: Utility-First Overload
TailwindCSS
is beloved for its utility-first approach and the way it keeps styles out of your JavaScript logic, but it introduces a whole different beast: verbose and cluttered markup.Instead of writing a single
className
, you might end up with long chains of utility classes, which can quickly become unmanageable and difficult to read.For example:
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Click me
</button>
This might seem fine for a single button, but as your components grow in complexity, your HTML will be littered with long strings of utility classes.
It also makes refactoring harder, since style definitions are scattered across different elements rather than centralized.
This can make debugging a pain, and for teams that value semantic, clean, and readable code, TailwindCSS
can feel chaotic.
If someone unfamiliar with TailwindCSS
needs to jump into your project, they’ll have to understand the utility classes before they can make sense of your markup.
Linaria & Vanilla Extract: The Build-Time Trade-Off
Linaria
& Vanilla Extract
take a different approach from styled-components
by statically extracting CSS at build time.While this is great for performance, it also means that if you need dynamic styling (e.g., based on props or theme), they can’t offer the same level of flexibility as
styled-components
.Since the CSS is extracted statically, dynamic styles that rely on JavaScript variables or props aren’t supported out of the box, meaning you’ll have to resort to inline styles or more complex workarounds.
import { css } from 'linaria';
const title = css`
font-size: 24px;
color: blue;
`;
const Title = () => <h1 className={title}>Hello Linaria!</h1>;
import { style } from '@vanilla-extract/css';
const button = style({
backgroundColor: 'blue',
color: 'white',
padding: '10px 20px',
borderRadius: '4px',
});
const Button = () => <button className={button}>Click Me</button>;
The same is true for PandaCSS
.
Its focus on a design token system works well for large-scale, design-system-driven projects, but if you’re working on a smaller project or want more on-the-fly flexibility like styled-components
’ dynamic styles, PandaCSS
can feel overly rigid.
Additionally, setting up and maintaining a token-based system adds another layer of complexity for teams just trying to get things done.
import { css } from '@pandacss/dev';
const button = css({
backgroundColor: 'blue.500',
color: 'white',
padding: '2',
borderRadius: 'md',
});
const Button = () => <button className={button}>Click Me</button>;
CSS Modules: Simple, But Limited
CSS Modules
work great for small, component-specific styles, but if you’re building a larger design system or need theming functionality, you’ll have to implement it manually or with additional libraries.This lack of built-in theming and dynamic styling can make CSS Modules feel limited compared to more powerful alternatives like
styled-components
or TailwindCSS
./* button.module.css */
.button {
background-color: blue;
color: white;
padding: 10px 20px;
border-radius: 4px;
}
/* Button.tsx */
import styles from './button.module.css';
const Button = () => <button className={styles.button}>Click Me</button>;
Honorable Mention
I want to also mention
Pigment CSS
, which was developed by the maintainers ofMaterial UI
, and will their main CSS solution in v6 ofMUI
.
It’s very similar to the other zero runtime packages listed here, and has a similar API tostyled-components
, but at the moment it’s still in v0 and might not be production-ready just yet.
It’s worth keeping an eye on though.import { styled } from '@pigment-css/react'; const Button = styled.button` font-size: 2rem; color: red; `; <Button> Click me! </Button>
Conclusion
I’ll always have a soft spot forstyled-components
, and I truly think its maintainers did a fantastic job with it.These days, the maintainers are struggling to figure if and how
styled-components
can even work with the new React Server Components, seeing as they’re heavily reliant on React’s Context API, which simply is not available in Server Components.This alone threatens to make
styled-components
a non-viable option for many future projects that will likely be built with RSC.However, while the alternatives to styled-components
each bring significant advantages, they aren’t without their own shortcomings.
Whether it’s TailwindCSS
’s verbose markup, Linaria
and Vanilla Extract
’s tooling complexity, PandaCSS
’s rigid token-based system, or CSS Modules
’ simplicity, every tool has trade-offs.
The key is to assess what fits your project best: do you need flexibility, performance, or ease of use? No solution is perfect, but each has its place in modern CSS architecture.
Personally, I never liked TailwindCSS
.
I realize that it’s very popular right now, and that the simplicity of utility classes allow it to easily integrate into many frameworks.
But every time I see those long lines of utility class names, I feel like I have to pick up a dictionary just to translate what’s going on.
And once I reach those complex components that have dozens of those classes all over the markup, I get the urge to delete the entire Git repo, just so have that abomination cease to exist.
This blog is written using Vanilla Extract
, and for the most part, I’ve enjoyed working with it.
The inability to do things dynamically is a bit annoying, but I believe most of the cases that demand it can be resolved with some predefined style options. Also, I’m not a fan of the object syntax for CSS.
If I had to pick a package for the modern project, I would either try PandaCSS
or maybe give Pigment CSS
a go once it’s more mature.
Then again, maybe this is all just over-engineering, and it’s all just leading us back to using plain old CSS files.
Whoever is brave enough to go back to that, let me know how that went, preferably before you’ve killed yourself.