Modern CSS architecture offers four main approaches — BEM naming conventions, CSS Modules for build-time scoping, CSS-in-JS libraries for dynamic runtime styles, and utility-first frameworks like Tailwind that eliminate custom CSS entirely — each with distinct trade-offs in performance, maintainability, and framework compatibility.
Block__Element--Modifier naming provides predictable, flat selectors without build tools, but requires team discipline and offers no automatic scoping.
Build-time class name scoping with zero runtime cost. Works natively with Next.js and Vite. A strong middle ground between conventions and full CSS-in-JS.
Libraries like styled-components add runtime overhead and are incompatible with React Server Components, driving the ecosystem away from this approach.
Tailwind compiles to static CSS with zero runtime, works with Server Components, and eliminates dead code through content-based purging — the default for modern React projects.
Choosing a CSS methodology is one of the most consequential architectural decisions in a frontend project. Each approach solves the same core problems — naming collisions, dead code, and maintainability — but with fundamentally different trade-offs.
BEM is a naming convention that uses flat, predictable selectors: .block__element--modifier. A card component becomes .card, its title becomes .card__title, and an active variant becomes .card--active.
BEM's strength is simplicity — it requires no build tools, works everywhere, and keeps specificity flat (single class selectors only). Its weakness is verbosity and the discipline it demands: nothing enforces the convention, and teams can drift. BEM works well for smaller projects and teams that want zero tooling overhead.
CSS Modules are regular CSS files where class names are locally scoped by the build tool. When you import a CSS Module, the build tool (webpack, Vite, Next.js) generates unique class names like .card_abc123, preventing collisions automatically:
/* Card.module.css */
.title { font-size: 1.5rem; }import styles from './Card.module.css';
<h2 className={styles.title}>Hello</h2>
// Renders: <h2 class="Card_title_abc123">Hello</h2>CSS Modules provide true encapsulation at build time with zero runtime cost. They work natively with Next.js, Vite, and webpack. The trade-off is separate files per component and no dynamic styling based on props (you must toggle class names instead). CSS Modules are an excellent middle ground between BEM and CSS-in-JS.
CSS-in-JS libraries co-locate styles with components in JavaScript, enabling dynamic styles based on props:
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
padding: ${props => props.size === 'lg' ? '16px' : '8px'};
`;Advantages: Component-scoped by default, dynamic styles via props, automatic critical CSS extraction, theming via context, and TypeScript support for style props.
Disadvantages: Runtime overhead (styles are injected into the DOM via JavaScript), increased bundle size (the library itself is 10-15KB gzipped), and incompatibility with React Server Components. Since Server Components cannot use context or inject client-side JavaScript, CSS-in-JS libraries like styled-components and Emotion cannot run on the server in the RSC model. This is the primary reason the React ecosystem has shifted away from CSS-in-JS.
Tailwind provides pre-defined atomic utility classes applied directly in markup:
<div class="flex items-center gap-4 rounded-lg bg-white p-6 shadow-md">
<h2 class="text-xl font-bold text-gray-900">Card Title</h2>
</div>Advantages: Zero runtime JavaScript (compiles to static CSS), works perfectly with React Server Components, eliminates naming decisions entirely, dead code elimination via content-based purging (only classes you actually use are included), consistent design tokens via config, and extremely fast development once the class vocabulary is learned.
Disadvantages: Verbose markup, initial learning curve for the class names, harder to extract complex animations, and can be harder to read for developers unfamiliar with the utility classes.
Tailwind CSS v4 introduced cascade layers internally, CSS-first configuration (replacing tailwind.config.js with @theme in CSS), and improved performance. It has become the default styling choice for new React and Next.js projects.
| Factor | BEM | CSS Modules | CSS-in-JS | Tailwind | |--------|-----|-------------|-----------|----------| | Runtime cost | None | None | Yes (10-15KB) | None | | Server Components | Yes | Yes | No | Yes | | Dynamic styles | No (class toggle) | No (class toggle) | Yes (props) | No (class toggle) | | Build tooling | None | Required | Required | Required | | Naming decisions | Manual | Per-file | Per-component | None | | Dead code elimination | Manual | Partial | Automatic | Automatic |
Choose BEM for small projects with no build tooling. Choose CSS Modules when you want encapsulation without opinions. Choose Tailwind for most modern React/Next.js projects (zero runtime, RSC compatible, fast development). Choose CSS-in-JS only if you have heavy dynamic styling needs and are not using Server Components.
Fun Fact
Tailwind CSS was initially rejected by many developers who thought utility classes were a step backward to inline styles. Creator Adam Wathan almost abandoned the project after harsh community feedback, but it went on to become the most popular CSS framework by GitHub stars, overtaking Bootstrap in 2023.