CSS Modules vs CSS-in-JS vs Atomic CSS: Which One to Choose for Your React.js Project?
In this blog post, I want to present different styling approaches in React.js projects. Many times, this decision is based on developers’ preferences. However, we should consider the type of the project, developers’ team experience, and workflow. Commonly, back-end or full-stack developers know how to code in React.js, but don’t have much experience in CSS development.
Currently, there are three popular approaches in React: CSS Modules, CSS-in-JS, and Atomic CSS. In many cases it’s not possible to give you a straight answer as to what’s the best approach, but I want to give you another perspective that can help you in this decision-making process.
CSS Modules and Sass
Back in the day, when I started with React development the most popular approach was Sass with BEM naming methodology. At that time options for styling were limited. CSS was designed for separate documents when single-page applications didn’t exist. CSS didn’t support variables, nested blocks and Sass was the best option at that time. Since the beginning of SPA development we have different requirements for implementing web applications styles.
When I was working on my first React project and needed to make a lot of layout changes, my sass code was chaotic. Even nowadays I can see the same mistakes when I join an existing React project that uses Sass for styling. In my opinion, developers who are using Sass with BEM or similar methodologies need to be experienced in css, and disciplined to keep their layout implementation maintainable and consistent.
One of the common “issues” with CSS in React projects is that importing a css code in a component is loaded into a global scope, which can result in conflicts. CSS Modules is a build step of a module bundler that scopes class names into a namespace.
I put CSS Modules and Sass with BEM into the same category, because both of them try to solve the same issue with CSS global namespacing. I believe this approach is still valid, but it can easily fail if React developers are not experienced in CSS development. It seems easy to maintain css in simple projects, but even engineers at big tech companies such as Facebook were struggling to maintain their css code base.
CSS-in-JS
Later, there was a new approach CSS-in-JS that is still very popular nowadays. I remember when I was working with styled-components for the first time. I was surprised by how easy it’s to make style code maintainable without any learning curve. Even back-end developers without any front-end experience can implement a maintainable layout. One of the main benefits of this approach is implementing dynamic styles through props and easy integration with Typescript:
import styled from '@emotion/styled/macro';
import { theme } from '../../../styles/theme';
const { sizes, colors } = theme;
type Side = "bottom" | "left" | "right" | "top";
interface ArrowProps {
arrowX: number;
arrowY: number;
staticSide: Side;
}
const gapFromAnchorElement = 4;
export const Arrow = styled.div<ArrowProps>`
position: absolute;
pointer-events: none;
left: ${({ arrowX }) => `${arrowX}px`};
top: ${({ arrowY }) => `${arrowY}px`};
${({ staticSide }) => ({
[staticSide]: `-${sizes.gap - gapFromAnchorElement - 2}px`,
...(staticSide === 'left' || staticSide === 'right'
? {
borderTop: `${sizes.gap - gapFromAnchorElement}px solid transparent`,
borderBottom: `${sizes.gap - gapFromAnchorElement}px solid transparent`,
}
: {}),
...(staticSide === 'left' ? { borderRight: `${sizes.gap - gapFromAnchorElement}px solid ${colors.White};` } : {}),
...(staticSide === 'right' ? { borderLeft: `${sizes.gap - gapFromAnchorElement}px solid ${colors.White};` } : {}),
})};
`;
In general, it’s a great choice, but there are 2 issues with this approach:
1. There is a lot of boilerplate code even in simple styles. For this reason productivity with this approach is decreasing significantly.
2. It’s not a good choice for SEO optimised React projects. Most CSS-in-JS libraries injects the generated stylesheet at the end of the head of the document during runtime. They are not able to extract styles into css files. It’s not possible to cache CSS. You can check more details on this topic here and here. There is an attempt to solve this by using Linaria, but further optimizations such as inline critical CSS and lazy loading can be very challenging or even not possible. For this reason, CSS-in-JS is not a good approach for projects where you need to optimize First Contentful Paint and other SEO performance metrics. For example, CSS-in-JS is not a good fit for e-commerce Next.js projects.
On the other hand, CSS-in-JS is a great choice for projects that require deep Typescript integration and SEO performance metrics don’t matter. Enterprise or data oriented projects is a good fit, because they require Typescript integration and you don’t care about caching css and SEO performance metrics.
Atomic CSS
Then, we have a very popular approach that is called Atomic CSS. In this approach, you use utility classes to implement styles. It’s easy to keep your style code maintainable without any learning curve. The main advantage compared to CSS-in-JS is that it significantly increases productivity. There is just one disadvantage: the code is less readable and for this reason a lot of developers don’t like this approach:
import { ICard, SearchItemStatus } from '../SearchResults';
export function ResultCard({
title,
snippet,
url,
status,
isFocused,
}: ICard & {
isFocused: boolean;
}) {
const cardColor =
status === SearchItemStatus.ACCEPTED
? 'bg-green-100 border-green-300'
: status === SearchItemStatus.REFUSED
? 'bg-yellow-100 border-yellow-300'
: status === SearchItemStatus.BANNED
? 'bg-red-100 border-red-300'
: 'bg-white border-gray-200';
return (
<div
className={`flex justify-start items-center ${cardColor} rounded-md border py-7 px-7 ${
isFocused ? 'outline-blue' : ''
}`}
>
<div className="flex flex-col items-start">
<div className="flex items-center mb-1 text-sm text-left">
<a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
<div className="mb-1 text-lg text-blue-700">{title}</div>
<p className="text-sm text-left">{snippet}</p>
</div>
</div>
);
}
If you have a team of 15+ full-stack developers, you can be sure there is at least one developer who doesn’t like this approach.
The most popular atomic CSS framework is Tailwind. It has its own preconfigured design system and theme configuration that you can customise based on your needs. Designers can use and customise the Tailwind Design Kit to build the design system. A well-defined design system with css framework and its css theme configuration is a big benefit of collaboration between developers and designers. If you use Tailwind, you need to use the Tailwind CSS IntelliSense plugin, because learning all utility classes wouldn’t make you more productive.
CSS Modules vs Atomic CSS
Currently, there are two camps:
CSS Module camp: These developers are experienced CSS developers. They are aware of the atomic CSS approach and its attempts before Tailwind. They don’t need it, because most of these devs started with CSS development before SPAs existed. They learned their lesson and know how to make CSS code maintainable and consistent.
Atomic CSS camp: These developers enjoy all benefits provided by Tailwind. They don’t mind the code readability and don’t need to be experienced in CSS development. Even developers from the CSS Module camp moved to this camp.
Conclusion
In this blog post, I wanted to highlight the pros and cons of using CSS modules vs CSS-in-JS vs atomic CSS in React projects. CSS-in-JS is the winner for specific types of projects. If you decide between CSS Modules and Atomic CSS, the decision is not so easy. You need to take into account how the team is experienced in CSS development and if you can benefit from atomic CSS framework such as Tailwind.
If you decide to go with CSS Modules with developers not so experienced in CSS, you can achieve great results. Stylelint can help you to set up specific rules for CSS development. In my next blog post, I will show you how to configure Stylelint and ESLint to don’t violate the design system. You don’t need to be afraid anymore that your React project won’t be synced with your design system. Stay tuned!
I would like to hear from you.
What’s your experience? Is there anything you are missing in this blog post?