One dilemma that developers commonly face when working with JavaScript frameworks is whether or not to use CSS-in-JS. It’s likely that you have worked with CSS-in-JS before if you’re a React developer.
Nowadays, CSS vs. CSS-in-JS is a hot topic. This is mostly because CSS-in-JS is getting called out for its performance concerns. However, there are also some new CSS features in the pipeline that should address some of these concerns in the near future.
The purpose of this article is to help you choose between CSS and CSS-in-JS for your upcoming projects in light of the current state of modern CSS and how it is likely to change in the future.
Jump ahead:
All code snippets and demos presented in this article feature React and CSS. Ensure that you are familiar with both these web technologies before moving forward.
Note that any JavaScript frontend framework or library can implement the idea of CSS-in-JS. This article uses React — by far, the most popular JavaScript frontend library — to discuss the application of CSS-in-JS, along with its notable pros and cons.
Before going further into what’s best and what’s not, let’s first discuss a bit about rendering problems caused specifically by CSS.
Traditionally, the browser loads HTML first, then CSS from all the external resources. After, the browser creates CSSOM using all the external and internal CSS information. Now, the browser is ready to provide styles to the rendered HTML according to the CSS cascade rules.
This process causes CSS to block the page from rendering and delays the first paint of the requested page. First paint is the event when the browser prints the first pixel on the screen for the page requested.
More than a half-second delay in the first paint poses a greater risk of user dissatisfaction and can negatively affect the app’s goals. The faster you can deliver CSS to the client, the better you can optimize the page’s time to first paint.
With an HTTP/2-powered app, multiple HTML, CSS, and JS files can load in parallel. This ability was limited with HTTP/1.1. Most modern browsers and websites now support HTTP/2, which minimizes render blocks caused by waiting for other files to load:
However, render-blocking also involves other factors besides file loading speed.
Let’s say a page of our app has a lot of CSS. It may contain selectors that aren’t even used but are there because we import one master CSS file on every page.
The above scenario basically describes how we are accustomed to directly consuming a CSS UI framework or a UI kit we created to quickly facilitate our design system. Not all the styles referenced from that framework or kit get used on every page. As a result, we end up with more junk in our final CSS styles for the page.
The more CSS, the longer it will take the browser to construct CSSOM, which results in completely unnecessary render blocking.
To counter this, splitting CSS into small chunks is very helpful. In other words, keep the global styles and critical CSS in one universal CSS file, then componentize everything else. This strategy makes much more sense and solves the unnecessary blocking problem:
The picture above shows the traditional way to create and manage separate CSS files for different components in React. Because each CSS file is directly attached to its respective component, it imports only when the relevant component is imported and disappears when that component is removed.
Now, there is one downside to this method. Let’s suppose our app contains 100 components, and other developers working on the same project have accidentally used the same class names in some of these CSS files.
Here, the scope of every CSS file for each component is global, so these accidentally duplicated styles would keep overriding each other and getting applied globally. A scenario like this will result in severe layout and design inconsistencies.
CSS-in-JS is said to fix this scoping issue. The upcoming segment reviews CSS-in-JS at a high level and discusses whether or not it solves the scoping problem effectively once and for all.
CSS-in-JS, in a nutshell, is an external layer of functionality that allows you to write CSS properties for components through JavaScript.
It all started in 2015 with a JavaScript library called JSS, which is still actively maintained. You have to provide the CSS properties to the selectors using JavaScript syntax, which then automatically applies those properties to their respective selectors once the page loads.
When JavaScript took over rendering and managing the frontend with libraries like React, a CSS-in-JS solution called styled-components emerged. Another increasingly popular way to do the same thing is by using the Emotion library.
We are going to demonstrate an example use case for CSS-in-JS with the styled-components library, as it is the most popular way to use CSS-in-JS in React.
In your React app, install the styled-components library using the below Yarn command. If you are using a different package manager, see the styled-components installation docs to find the appropriate installation command:
yarn add styled-components
After installing the styled-components library, import the styled
function and use it as shown in the code below:
import styled from "styled-components"; const StyledButton = styled.a` padding: 0.75em 1em; background-color: ${ ({ primary }) => ( primary ? "#07c" : "#333" ) }; color: white; &:hover { background-color: #111; } `; export default StyledButton;
If you don’t have access to a React environment, here’s a CodePen demo for you to see the above code in action:
See the Pen Dropdown menus with CSS by Rahul Chhodde (@_rahul)
on CodePen.
The code above demonstrates how to style a button-link component in React. This styled component can now be imported anywhere and used directly to build a functional component without having to worry about the styles:
import StyledButton from './components/styles/Button.styled'; function App() { return ( <div className="App"> ... <StyledButton href="...">Default Call-to-action</StyledButton> <StyledButton primary href="...">Primary Call-to-action</StyledButton> </div> ); } export default App;
Note that the styles applied to the styled components are locally scoped, which eliminates the cumbersome need to be mindful of CSS class naming and the global scope. In addition, we can add or remove CSS dynamically based on the props supplied to our component or any other logic demanded by an app feature.
A JavaScript developer may prefer to style things with CSS-in-JS rather than going through CSS classes. The biggest problem the CSS-in-JS approach solves is the global scope. It also has some other advantages that make a lot of sense if you are a JavaScript developer.
Let’s explore some of these benefits now.
Since styles are available in a local scope, they are not prone to clashing with the styles of other components. You don’t even have to worry about naming things strictly to avoid style clashes.
Styles are written exclusively for one component without prepending child selectors, so specificity issues are rare.
Conditional CSS is another highlight of CSS-in-JS. As the button example above demonstrates, checking for prop values and adding suitable styles is way cooler than writing separate CSS styles for each variation.
CSS-in-JS helps you keep the specificity of CSS declarations to the lowest, as the only thing you style with it is the element itself. The same applies to creating component variations, where you can check for prop object values and add dynamic styling when required.
Theming apps with custom CSS properties makes sense. In the end, you will have to move to the JavaScript side and write the logic to switch and remember the theme based on user input.
CSS-in-JS allows you to write theming logic entirely in JavaScript. With the styled-components ThemeProvider
wrapper, you can quickly color-code themes for components. Take a look at this CodePen example to see component theming with styled-components in action:
See the Pen Component theming with the styled-components library by Rahul Chhodde (@_rahul)
on CodePen.
Considering the features and advantages CSS-in-JS offers, a JavaScript developer may find CSS-in-JS more convenient than managing hundreds of CSS files.
The fact remains, however, that one must have a good understanding of both JavaScript and CSS to effectively manage and maintain huge projects powered by CSS-in-JS.
CSS-in-JS does solve the scoping problem very well. But as we discussed initially, we have much bigger challenges — like render-blocking — that directly affect the user experience. Along with that, there are some other issues that the concept of CSS-in-JS still has to address.
CSS-in-JS will execute JavaScript to parse CSS from JavaScript components, and then inject these parsed styles into the DOM. The more components more will be the more time taken by the browser for the first paint.
CSS caching is often used to improve successive page load times. Since no CSS files are involved when using CSS-in-JS, caching is a big problem. Also, dynamically generated CSS class names make this issue even more complicated.
With the regular componentized CSS approach, it’s easy to add support for preprocessors like SASS, Less, PostCSS, and others. The same is not possible with CSS-in-JS.
CSS-in-JS is based on the idea of parsing all style definitions from JavaScript into vanilla CSS and then injecting the styles into the DOM using style blocks.
For each component styled with CSS-in-JS, there could be 100 style blocks that must be parsed first, then injected. Simply put, there will be more overhead costs.
As we already know, we can add CSS-in-JS functionality with an external library. A lot of JavaScript will be included and run before actual CSS parsing, as parsing styles from JavaScript to CSS styles depends on a library like styled-components.
A lot of native CSS and SCSS features are missing with CSS-in-JS. It may be very challenging for developers who are used to CSS and SCSS to adapt to CSS-in-JS.
Most of the UI and component libraries don’t support the CSS-in-JS approach right now, as it still has a lot of issues to address.
The problems discussed above may collectively contribute to a low-performant, hard-to-maintain product with several UI and UX inconsistencies.
The CSS-in-JS solution is ideal when you are dealing with a smaller application for which performance is a lower priority. It may not be ideal when dealing with a performance-critical application with a huge design system.
As an app grows bigger, using CSS-in-JS can get complicated easily, considering all the drawbacks of this concept. A lot of work goes into converting a design system into CSS-in-JS, and in my opinion, no JavaScript developer would want to deal with that.
A CSS Module is a CSS file in which all the properties are scoped locally by default in the rendered CSS. JavaScript processes the CSS Module files further and encapsulates their style declarations to solve the scoping issue.
To use CSS Module, you need to name your CSS files with a .module.css
extension and then import them into JavaScript files. The below code snippet provides a basic example of how to use CSS Module:
import styles from './Button.module.css'; export default function Button(props) { return ( <a href={props.href ? props.href : '#'} className={styles.btn} > {props.name} </a> ); }
Take a look at this StackBlitz example for implementing CSS Modules in React. This example shows how to use CSS Modules to fix the scoping problem.
In the StackBlitz example, notice how the same class names in Button.module.css
and AnotherButton.module.css
are processed and optimized intelligently to prevent naming conflicts.
The most significant benefit that CSS Module offers is removing the reliance on CSS-in-JS to fix the scoping and specificity problems. If we can fix the scoping and specificity problems by keeping CSS as traditional as possible, CSS-in-JS will be more work than necessary.
As demonstrated in the example above, CSS Module successfully solves the scoping problem we have with traditional, old-style CSS. As the rules are loosely written in CSS Module files, it’s rare to observe any specificity problems.
Keeping separate CSS files may appear to be a limitation. However, this method actually promotes better organization. For example, here’s how I organize components by separating them into their own folders:
- Project - src - components - Button - Button.jsx - Button.modules.css - Carousel - Carousel.jsx - Carousel.modules.css
The minified CSS files generated with the final build can be cached by the browser to improve the successive page load times.
It’s easy to add support for CSS preprocessors like PostCSS, SASS, Less, and others. However, you have to rely on additional packages to do so.
If you know how CSS works, you can use CSS Module without learning anything new besides the few points that we discussed above in the intro segment.
You won’t need to add additional packages to use CSS Modules. All major frameworks and libraries provide inbuilt support.
While CSS Module offers many benefits, it’s not a perfect solution. Below are some considerations to keep in mind.
:global
propertyWhen targeting selectors in the global scope, you must use the :global
rule. This not a part of CSS specifications but is used by JavaScript to label global styles.
With CSS Module, all the declarations go into separate CSS files. It’s therefore impossible to implement dynamic styles like CSS-in-JS, as we can’t implement any JavaScript in CSS files.
You can’t omit the usage of CSS files with CSS modules in your components. The only possible way to use CSS modules is to maintain and import external CSS files.
To use CSS Modules with TypeScript, you have to add module definitions in the index.d.ts
file or use a webpack loader:
/** index.d.ts **/ declare module "*.module.css"; // TS module for CSS Module files declare module "*.module.scss"; // TS module for CSS Module files in SCSS format
Using CSS Module is a good choice if you have a performance-critical application with a large UI. Since everything offered by CSS Module is ultimately based on traditional, non-experimental usage, this method makes it easier to monitor and fix performance.
The CSS Module files are simple to adapt code from any CSS framework of your choice since all you’re dealing with is CSS. Some basic knowledge of CSS is sufficient for this task, as discussed previously.
In the introduction, I mentioned how some modern CSS features may help solve the scoping problem in the future without relying on CSS Module, CSS-in-JS, or any other JavaScript solution.
New and planned features — such as scoping directives and the @scope
pseudo-element — aim to address the old issues with traditional CSS. This, in turn, may reduce the need for developers to turn to methods like CSS-in-JS as workarounds for those issues.
Let’s take a look at how the current draft for scoped CSS could solve the problems with CSS-in-JS and even CSS Module. For a full list of other modern CSS features, check out the State of CSS 2022.
After the strange introduction and removal of <style scope>
from the CSS specifications, the current draft for scoped CSS looks good enough to define scoping premises for elements by writing CSS rules.
Its current status involves using a directive and a pseudo-class to control the provision of scoping for a given element. Here is a rough picture of how it will lock an element’s scope within a boundary and maintain it regardless of the cascade’s rules of scoping:
<div class="card"> <img src="..."> <div class="content"> <p>...</p> </div><!-- .content --> </div><!-- .card --> <style> @scope (.card) { :scope { display: grid; ... } img { object-fit: cover; ... } .content { ... } } </style>
This new feature may remove the need for CSS Module or CSS-in-JS to resolve the scoping problem. We have to wait and see until it becomes available in our browsers.
Above, we discussed how CSS render-blocking can be a major performance issue for your web apps. We then discussed some solutions to fix this issue, which led us to explore CSS-in-JS, CSS Modules, and the current status of the official in-progress draft for new scoped CSS features.
Developers who like JavaScript love CSS-in-JS because it covers almost all styling aspects with JavaScript. On the other hand, those who like CSS — and want the current technologies to support developers and end users equally — may prefer CSS Module.
I hope you enjoyed this article. Let me know your thoughts, questions, and suggestions in the comments.
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.