CSS Architecture at Scale
CSS at scale is an organizational problem, not a styling problem. The question isn't "how do I center a div" â it's "how do 20 engineers write CSS that doesn't collapse into specificity warfare."
The Core Problem
CSS has three properties that make it hard at scale:
- Global scope â every selector can affect every element
- Specificity conflicts â the cascade creates implicit dependencies
- Dead code uncertainty â you can never safely delete a rule
Every architecture below is a different answer to these three problems.
BEM (Block Element Modifier)
A naming convention that creates scope through discipline:
.card {}
.card__title {}
.card__title--highlighted {}
.card__body {}Strengths: Zero tooling required, predictable specificity (everything is a single class), easy to trace in DevTools.
Weaknesses: Verbose, relies on team discipline, no compile-time guarantees. One rogue !important breaks the contract.
Best for: Static sites, teams with strong conventions, WordPress/CMS projects.
CSS Modules
File-scoped CSS with compile-time class name hashing:
/* Button.module.css */
.root {
padding: 0.5rem 1rem;
}
.primary {
background: var(--color-primary);
}import styles from './Button.module.css';
function Button({ variant }) {
return <button className={`${styles.root} ${styles[variant]}`} />;
}The compiler transforms .root into .Button_root_x7kf2, guaranteeing no collisions.
Strengths: True scope isolation, standard CSS (no runtime), works with PostCSS/Sass, small bundle impact.
Weaknesses: Composing styles across modules requires composes or utility classes, dynamic styling needs inline styles or CSS variables.
Best for: Next.js apps, component libraries, teams that want scope without a runtime.
CSS-in-JS (styled-components, Emotion)
Styles defined in JavaScript, scoped to components:
const Button = styled.button<{ variant: 'primary' | 'secondary' }>`
padding: 0.5rem 1rem;
background: ${(p) => p.variant === 'primary' ? 'var(--blue)' : 'var(--gray)'};
`;Strengths: True colocation, dynamic styling based on props, type-safe with TypeScript, dead code elimination through tree shaking.
Weaknesses: Runtime cost (style injection on render), SSR complexity (double rendering for style extraction), bundle size overhead.
The shift: The industry is moving toward zero-runtime CSS-in-JS (Vanilla Extract, Panda CSS, StyleX) that compile to static CSS at build time, getting the DX of CSS-in-JS without the runtime cost.
Tailwind CSS
Utility-first approach â style directly in markup:
function Card({ children }) {
return (
<div className="rounded-lg shadow-md p-6 bg-white dark:bg-gray-800">
{children}
</div>
);
}Strengths: No naming decisions, consistent design tokens, tiny production CSS (purged unused utilities), fast prototyping.
Weaknesses: Verbose markup, harder to scan for semantic meaning, responsive/state variants get long, requires build tooling.
Best for: Product teams shipping fast, design-system-constrained projects, solo developers.
Design Tokens
Regardless of CSS approach, design tokens create a shared language between design and engineering:
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--radius-md: 0.5rem;
--font-body: 'Inter', system-ui, sans-serif;
}Tokens can be exported from Figma, consumed by any CSS methodology, and serve as the contract between design and code. In mature organizations, tokens are the source of truth â not Figma files, not CSS variables.
Decision Framework
| Factor | BEM | CSS Modules | CSS-in-JS | Tailwind |
|---|---|---|---|---|
| Scope isolation | Convention | Compile-time | Runtime | Utility specificity |
| Runtime cost | None | None | Medium | None |
| Dynamic styling | Limited | CSS vars | Native | Class toggling |
| TypeScript integration | None | Typed modules | Full | Plugin support |
| Learning curve | Low | Low | Medium | Medium |
| Team scalability | Requires discipline | Good | Good | Good with design system |
Interview Signal
The question "how would you structure CSS for a large application?" tests architectural thinking. Senior answers include:
- Constraint acknowledgment â team size, existing codebase, performance requirements
- Token-first thinking â design tokens as the foundation regardless of methodology
- Trade-off articulation â not "Tailwind is best" but "Tailwind optimizes for velocity at the cost of markup readability"
- Migration awareness â knowing that CSS architecture choices are expensive to change