CSS Architecture at Scale: BEM, CSS Modules, and Why Tailwind Sidesteps Both
BEM and CSS Modules solve the same scoping problem Tailwind solves, in earlier, more manual ways worth understanding — the problem is the actual point.
BEM and CSS Modules both predate Tailwind's mainstream adoption, and both are attempts to solve the exact same problem — CSS's lack of natural scoping — with two different, earlier strategies worth understanding, because the underlying problem is the actually transferable idea, not either specific syntax.
BEM: solving scoping through naming discipline
.card { } /* Block */
.card__title { } /* Element — belongs to card */
.card--featured { } /* Modifier — a variant of card */Block-Element-Modifier is a pure naming convention — no tooling required, just discipline. The verbose, structured class names (`card__title` instead of a bare `.title`) exist specifically to make collisions between unrelated components extremely unlikely, purely by making names long and specific enough that two different components are unlikely to pick the exact same one. The real cost: it's discipline, not enforcement — nothing stops a tired developer from breaking the convention under deadline pressure, and nothing catches it when they do.
CSS Modules: solving scoping through tooling
/* card.module.css */
.title { font-weight: 600; }
/* Build tool renames this to something like .card_title__a3f2x at build
time, and only the component that imported this exact file gets a
reference to the correct generated name — true, enforced scoping */A build step rewrites every class name into something unique per file, and hands the component a mapping object — genuine, build-enforced scoping, not just a naming convention someone has to remember to follow. The cost: still plain CSS syntax underneath, so all the specificity and cascade behavior from earlier in this series still applies inside a module, just with collision risk removed.
Why Tailwind sidesteps the entire category of solution
BEM and CSS Modules both still start from "write a custom class, then solve the scoping problem that creates." Tailwind's utility classes are shared and pre-scoped by design — `p-6` never means something different in two different components, because it was never meant to describe a component-specific concept in the first place, only a single, universal, unambiguous CSS property value. It's not a better answer to the same question BEM and CSS Modules ask; it's opting out of the question by not creating custom, collision-prone names at all.