You've spent hours configuring your React application's styling approach. You opted for styled-components because everyone seemed to be using it. But now your bundle sizes are bloating, your render times are slowing, and you're wondering if you made the right choice. That nagging feeling grows stronger each time you see a Reddit thread questioning CSS-in-JS.
As one developer lamented: "Some say that CSS-in-JS turned out to be a bad solution for modern day problems." This sentiment reflects a growing concern in the front-end community about the approach that once promised to revolutionize how we style web applications.
The Evolution of CSS-in-JS
CSS-in-JS emerged as a solution to the limitations of traditional CSS in component-based architectures. In the early days of React, developers struggled with CSS's global scope and the inability to easily tie styles to specific components. Libraries like styled-components and Emotion offered a new paradigm: writing CSS directly within JavaScript.
// Styled-components example
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 15px;
border-radius: 4px;
`;
This approach quickly gained popularity. According to the State of CSS 2020 survey, approximately 40% of developers reported using styled-components, making it one of the most widely adopted styling solutions in the React ecosystem.
The evolution didn't stop there. As developers encountered limitations and performance issues with first-generation CSS-in-JS libraries, new solutions emerged. Zero-runtime libraries like Linaria began offering the developer experience of CSS-in-JS without the performance overhead, while utility-based approaches like Tailwind CSS gained traction for their productivity benefits.
Visual representation of CSS-in-JS for better understanding
The Good: Advantages of CSS-in-JS
Despite growing criticism, CSS-in-JS offers several compelling advantages that explain its continued popularity:
1. Scoped Styles and Isolation
CSS-in-JS automatically generates unique class names, preventing style collisions and eliminating the need for complex naming conventions like BEM (Block Element Modifier). This isolation is particularly valuable in large teams or when working with third-party components.
2. Dynamic Styling Based on Props
One of the most powerful features is the ability to generate styles based on component props:
const StyledButton = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
opacity: ${props => props.disabled ? 0.5 : 1};
`;
This enables a level of dynamism that's cumbersome to achieve with traditional CSS, making theme implementation significantly easier.
3. Colocated Styles with Components
CSS-in-JS allows developers to keep styles and component logic together, improving maintainability and making components more portable. As one developer noted: "I feel like it's just way easier, I don't have to think about how to organize and structure my CSS because it already follows the same structure and flow as my regular components."
4. Enhanced Developer Experience
Modern CSS-in-JS libraries offer excellent TypeScript integration, autocompletion, and linting support. They also handle vendor prefixing automatically, saving development time and reducing potential bugs.
The Bad: Performance Concerns and Complexity
Despite these advantages, a growing chorus of developers express concerns about CSS-in-JS, particularly regarding performance and unnecessary complexity.
1. Runtime Performance Overhead
The most significant criticism of first-generation CSS-in-JS libraries is their runtime overhead. As one developer discovered through testing: "Styled components had worse load and re-render time, we can say 100ms for simplicity. Linaria (styled syntax) had around 90-95ms. Linaria (CSS syntax) had around 30ms pretty much the same as if we didn't have any styling."
This performance penalty stems from several factors:
Style parsing and injection at runtime
Style recalculation when props change
Additional JavaScript execution before styles are applied
For applications prioritizing performance, especially those targeting low-powered devices, this overhead can be problematic.
2. Bundle Size Implications
CSS-in-JS libraries add to your JavaScript bundle size. For example:
styled-components: ~12.7 kB minified and gzipped
emotion: ~7.9 kB minified and gzipped
While these sizes may seem small, they contribute to the overall JavaScript payload that must be downloaded, parsed, and executed before your application becomes interactive.
3. Memory Usage
Runtime CSS-in-JS solutions can increase memory usage, particularly in complex applications with many styled components. The dynamic style generation creates additional objects that must be managed by the JavaScript engine.
Illustrative example showing the differences between CSS and CSS-in-JS
4. Portability Concerns
Some developers worry about the portability of CSS-in-JS code: "Additionally it makes CSS extremely non-portable because of the new syntax." When styles are tightly coupled to a specific CSS-in-JS library, migrating to another approach can be challenging.
The Future: Zero-Runtime and Hybrid Approaches
As developers recognize the trade-offs of runtime CSS-in-JS, the community has developed solutions aimed at preserving the developer experience while eliminating performance concerns.
Zero-Runtime CSS-in-JS with Linaria
Linaria represents a significant step forward in CSS-in-JS evolution. It allows developers to write CSS-in-JS syntax but extracts the styles to static CSS files at build time.
// Linaria example
import { styled } from '@linaria/react';
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 15px;
border-radius: 4px;
`;
The extracted CSS looks like this:
.Button_ab12cd {
background: var(--Button_background_ab12cd);
color: white;
padding: 10px 15px;
border-radius: 4px;
}
With CSS custom properties (variables) handling the dynamic aspects:
// Runtime code after extraction
Button.className = 'Button_ab12cd';
document.documentElement.style.setProperty(
'--Button_background_ab12cd',
props.primary ? 'blue' : 'gray'
);
This approach dramatically improves performance while maintaining most benefits of CSS-in-JS:
No runtime parsing of CSS
Styles are cached by the browser
Critical CSS can be included in the initial HTML
As one developer reported after switching to Linaria: "Linaria (CSS syntax) had around 30ms pretty much the same as if we didn't have any styling," showing a significant improvement over styled-components.
Panda CSS: A Modern Approach to Zero-Runtime Styling
Panda CSS represents the next evolution in styling solutions. It combines the type safety and component-focused approach of CSS-in-JS with zero-runtime performance and utility-first principles.
// Panda CSS example
import { css } from '@pandacss/react';
function App() {
return (
<div className={css({ padding: '20px', color: 'blue.500' })}>
Hello, Panda!
</div>
);
}
Key features of Panda CSS include:
Atomic CSS generation: Creates optimized, reusable CSS utility classes
Design token system: Built-in support for design systems and themes
Static extraction: All styles are generated at build time
Type safety: Full TypeScript support for style properties and theme values
Style recipes: Reusable style patterns with variants
This approach addresses many pain points expressed by developers while maintaining excellent performance characteristics.
CSS Modules: The Stable Middle Ground
While new zero-runtime CSS-in-JS libraries continue to emerge, many developers are finding that CSS Modules provide an excellent balance of performance and developer experience.
/* Button.module.css */
.button {
background-color: blue;
color: white;
padding: 10px 15px;
border-radius: 4px;
}
.secondary {
background-color: gray;
}
import styles from './Button.module.css';
function Button({ secondary, children }) {
return (
<button
className={`${styles.button} ${secondary ? styles.secondary : ''}`}
>
{children}
</button>
);
}
CSS Modules offer several compelling advantages:
Zero runtime overhead: Styles are processed at build time
Scoped class names: Automatic unique class names prevent collisions
Familiar CSS syntax: No need to learn new styling paradigms
Excellent performance: Full browser optimization of CSS
Simplicity: Minimal abstraction and tooling requirements
As one developer enthusiastically reported: "We just switched, or in the process of, from styled to CSS modules. I'm not looking back." Another added: "I seriously haven't found anything better than just CSS modules. They're so easy to use and you don't have to crowd your class names like tailwind."
Visual example showing the organization of CSS files for different components in a React application
Making the Right Choice: CSS-in-JS vs. CSS Modules
With the landscape of styling solutions constantly evolving, how do you choose the right approach for your project? Here are some guidelines to help you decide:
When to Choose CSS-in-JS
CSS-in-JS remains a strong choice when:
Dynamic theming is critical: If your application requires complex runtime theming, CSS-in-JS offers the most seamless experience.
Team is familiar with the ecosystem: For teams already proficient with CSS-in-JS, switching may introduce unnecessary friction.
Component library development: When building reusable component libraries, CSS-in-JS can offer better encapsulation.
You can use zero-runtime solutions: Libraries like Linaria or Panda CSS provide the CSS-in-JS developer experience without the performance penalties.
When to Choose CSS Modules
CSS Modules shine in these scenarios:
Performance is a top priority: Without runtime overhead, CSS Modules offer optimal rendering performance.
Leveraging browser caching: CSS Modules allow browsers to cache styles separately from JavaScript.
Simpler mental model: Teams more comfortable with traditional CSS benefit from the familiar syntax and concepts.
Easier onboarding: New team members familiar with CSS can quickly become productive without learning library-specific APIs.
Hybrid Approaches
Many successful projects combine multiple styling approaches:
Base styles with utility classes: Use CSS Modules or global CSS for foundational styles, with utility classes (via Tailwind or custom utilities) for quick adjustments.
Component-specific styles with global themes: Maintain theme variables in global CSS custom properties while using CSS Modules for component-specific styles.
Varying by component type: Use CSS-in-JS for highly dynamic components while using CSS Modules for static UI elements.
The Developer Experience Factor
Beyond technical considerations, developer experience plays a crucial role in the ongoing debate between CSS-in-JS and alternatives.
The Appeal of CSS-in-JS
Despite performance concerns, many developers continue to choose CSS-in-JS for its developer experience:
Colocated concerns: Keeping styles with component logic simplifies reasoning about components.
TypeScript integration: First-class type support for styles, themes, and props.
Abstraction capabilities: Ability to create styled primitives and component variants.
Familiar context: JavaScript developers can stay in their preferred language environment.
As one developer noted: "I feel like it's just way easier, I don't have to think about how to organize and structure my CSS because it already follows the same structure and flow as my regular components."
The Simplicity Appeal of CSS Modules
On the other side, CSS Modules advocates emphasize:
Simplicity: No additional abstractions to learn beyond CSS itself.
Separation of concerns: Clear distinction between styling and logic.
Performance by default: No need to optimize away runtime overhead.
Future-proof approach: Less dependency on specific libraries or frameworks.
This sentiment is captured by a developer who stated: "CSS modules π I'd argue this is the most stable and 'future proof' technique that solves the scoping issue with vanilla CSS."
Modern Styling Alternatives
Beyond the CSS-in-JS vs CSS Modules debate, several other approaches deserve consideration:
Utility-First CSS with Tailwind
Tailwind CSS has gained massive popularity by providing atomic utility classes that can be composed directly in HTML/JSX:
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Click me
</button>
While some developers find the approach clutters markup, others value the productivity gains. To mitigate the potential for class name bloat, many teams combine Tailwind with abstraction libraries like Class Variance Authority (CVA):
import { cva } from 'class-variance-authority';
const button = cva([
'font-bold py-2 px-4 rounded',
], {
variants: {
intent: {
primary: ['bg-blue-500', 'hover:bg-blue-700', 'text-white'],
secondary: ['bg-gray-500', 'hover:bg-gray-700', 'text-white'],
},
size: {
small: ['py-1', 'px-2', 'text-sm'],
large: ['py-3', 'px-6', 'text-lg'],
}
},
defaultVariants: {
intent: 'primary',
size: 'medium',
}
});
function Button({ intent, size, children }) {
return (
<button className={button({ intent, size })}>
{children}
</button>
);
}
CSS Custom Properties for Dynamic Styling
Modern CSS has evolved significantly with custom properties (CSS variables), enabling dynamic styling without JavaScript:
:root {
--primary-color: #3490dc;
--secondary-color: #ffed4a;
}
.button {
background-color: var(--primary-color);
color: white;
}
.button.secondary {
background-color: var(--secondary-color);
color: black;
}
This approach can be combined with CSS Modules for scoped, dynamic styling with minimal runtime overhead.
Future Trends in Styling Solutions
Looking ahead, several trends are likely to shape the future of web styling:
1. Continued Optimization for Performance
As web performance becomes increasingly important for SEO and user experience, we'll see continued innovation in styling solutions that minimize runtime overhead while preserving developer experience.
2. Convergence of Approaches
The lines between different styling paradigms are blurring. Libraries like Panda CSS combine the type safety of CSS-in-JS, the performance of CSS Modules, and the utility approach of Tailwind.
3. Leveraging Modern CSS Features
As browser support for modern CSS features like container queries, cascade layers, and nesting improves, styling solutions will increasingly leverage these capabilities without requiring JavaScript.
4. Design System Integration
Styling solutions will continue to evolve to support design systems more effectively, with better token support, component variant management, and theme customization.
Conclusion
The CSS-in-JS landscape has evolved significantly since the introduction of styled-components and Emotion. While these libraries solved critical problems for component-based architectures, their runtime performance cost has led to a reevaluation of styling approaches.
For teams starting new projects or considering a styling migration, the decision between CSS-in-JS and alternatives should consider:
Performance requirements: If optimal performance is critical, zero-runtime solutions like CSS Modules, Linaria, or Panda CSS offer clear advantages.
Developer experience preferences: Consider your team's familiarity with different styling paradigms and the learning curve associated with each approach.
Project complexity: More complex UIs with dynamic theming may benefit from more sophisticated styling solutions, while simpler applications can thrive with CSS Modules.
Future maintenance: Consider the long-term maintainability and potential migration paths as styling technologies continue to evolve.
As one developer wisely noted: "CSS was already solved through CSS Modules." While this may be an oversimplification, it highlights an important truth: sometimes the simpler solution is the better one.
Whether you choose CSS-in-JS, CSS Modules, or another approach, the styling ecosystem continues to evolve rapidly, offering developers increasingly sophisticated tools to create beautiful, performant user interfaces.
The future of CSS-in-JS is likely to be more diverse, with a range of specialized tools optimized for different use cases, rather than a one-size-fits-all solution. By understanding the trade-offs of each approach, you can make informed decisions that best serve your application's needs and your team's preferences.