10 minutes read
Styled Components: How the Mighty Have Fallen
(Under the Weight of Their Own Runtime)
Cover image

Pros

Cons


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 of styled-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 <head> of your document, potentially bloating your JavaScript bundle and slowing down load times as your app grows.

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 how Vanilla 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 about styled-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 to styled-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 of Material UI, and will their main CSS solution in v6 of MUI.
It’s very similar to the other zero runtime packages listed here, and has a similar API to styled-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 for styled-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.