CSS · Architecture
CSS architecture (BEM, ITCSS)
⚡ TL;DR — CSS at scale has two enemies: specificity wars and unpredictable source order. BEM defeats the first by keeping every selector at a flat
0,1,0; ITCSS defeats the second by ordering your stylesheet from generic and far-reaching to specific and narrow, so the cascade flows one direction.
🧠 Mental model
CSS is a global stylesheet with no module system and a cascade that resolves conflicts by specificity, then source order. On a small site that's fine. At scale it rots: someone adds #sidebar .nav a.active to win a fight, the next person needs !important to beat that, and now every override is an escalation. The stylesheet becomes a place where the last, most-specific rule wins by accident rather than intent.
Architecture is the discipline that makes the cascade predictable:
- BEM is a naming convention.
block__element--modifiermeans almost every selector is a single class — specificity0,1,0, flat and equal. When specificity is uniform, conflicts are decided by source order, which you control, instead of a specificity arms race you don't. - ITCSS (Inverted Triangle CSS) is a source-order convention. You split CSS into layers stacked from lowest specificity/widest reach at the top to highest specificity/narrowest reach at the bottom, so specificity only ever climbs as you read down the file.
Together: BEM keeps specificity flat within a layer; ITCSS keeps specificity monotonically increasing across layers.
⚙️ How it actually works
BEM anatomy — the naming maps to structure, not to the DOM tree:
.card /* Block: standalone component */
.card__title /* Element: a part, meaningless outside the block */
.card--featured /* Modifier: a variant/state of the block */
The rule that catches people: an element is always one level deep. .card__body__title is wrong — BEM names describe belonging to a block, not DOM nesting. If a part needs its own parts, it's probably its own block.
ITCSS layers, top → bottom (reach shrinks, specificity grows):
| Layer | Contains | Selector type |
|---|---|---|
| Settings | design tokens, $/-- variables |
none (no output) |
| Tools | mixins, functions | none (no output) |
| Generic | reset, box-sizing, normalize |
*, element |
| Elements | bare h1, a, p defaults |
element 0,0,1 |
| Objects | layout patterns (.o-media) |
class 0,1,0 |
| Components | UI (.card, .btn) — most of your CSS |
class 0,1,0 |
| Utilities | single-purpose overrides (.u-hidden) |
class, sometimes !important |
Because everything flows one way, a component never has to out-specify a reset, and a utility (bottom of the file) reliably wins over a component. Modern CSS makes this explicit: @layer gives you ITCSS's ordering as a first-class feature — you declare @layer generic, objects, components, utilities; and layer order beats specificity entirely, so even a #id in an earlier layer loses to a class in a later one.
💻 Code
/* ❌ The specificity war: each rule escalates to beat the last */
#page .sidebar ul li a.active { color: red; } /* 1,2,3 */
.nav a.active { color: red !important; } /* nuclear option */
/* ✅ BEM: flat, equal specificity — source order (or @layer) decides */
.nav__link { color: inherit; }
.nav__link--active { color: red; } /* both 0,1,0; the modifier just comes later */
/* ✅ ITCSS made literal with cascade layers — order wins over specificity */
@layer generic, elements, objects, components, utilities;
@layer components { .btn { padding: 8px 16px; background: navy; } }
@layer utilities { .u-p-0 { padding: 0; } } /* always beats .btn, no !important */
⚖️ Trade-offs
- BEM is verbose, and that's mostly a feature. Long class names read as documentation, are trivially greppable, and make dead CSS deletable because a class maps to exactly one component. The cost is HTML noise and no compile-time guarantee the class exists.
- BEM doesn't scope anything — it's a convention, not enforcement. Two engineers can both invent
.cardand collide. CSS Modules / scoped styles solve that mechanically; BEM relies on discipline. - ITCSS is a methodology, not a tool — its value is entirely in team adherence. One person dropping an
#idselector into the Components layer punctures the whole model. Cascade layers finally enforce it in the engine. - When NOT to reach for this: a small app or a component framework (React + CSS Modules / Tailwind) already solves scoping and locality, so full ITCSS+BEM ceremony is overkill. These conventions earn their keep in large, long-lived, global stylesheets.
💣 Gotchas interviewers probe
- "Why is BEM's flat specificity the point?" Because equal specificity means the cascade becomes predictable — the winner is decided by source order you control, not a specificity race. That's the senior framing.
block__el__el(nested elements) is an anti-pattern. BEM elements are one level deep by definition; deep nesting means you've missed a block boundary.- Objects vs Components confusion. Objects are unstyled structural patterns (a media object, a grid) reusable everywhere; Components are skinned UI. Mixing them is the most common ITCSS mistake.
@layerinverts your intuition: a rule in a later layer beats a more specific rule in an earlier layer. Unlayered styles beat all layered ones. People get burned migrating.- Utilities with
!importantare fine only because they're the last, narrowest layer — the one place the escalation is intentional and bounded.
🎯 Say this in the interview
"At scale, CSS breaks down for two reasons: specificity wars and unpredictable source order. BEM addresses the first —
block__element--modifierkeeps almost every selector at a flat0,1,0, so instead of escalating#id .class .classto win, everything has equal weight and the cascade is decided by order, which I control. ITCSS addresses the second by structuring the stylesheet from generic and wide-reaching at the top — resets, elements — down to specific and narrow at the bottom — components, then utilities — so specificity only ever climbs as you read down and a utility reliably wins without!important. The modern version of ITCSS is@layer, which enforces that ordering in the engine: layer order beats specificity, so a class in a later layer can beat an#idin an earlier one. I'd combine BEM for naming, layers for ordering, and reserve!importantfor the utilities layer only."
🔗 Go deeper
- getbem.com — BEM — the canonical block/element/modifier definition and rationale.
- CSS-Tricks — BEM 101 — pragmatic naming, with the nested-element anti-pattern.
- ITCSS — Harry Roberts — the inverted triangle from its author.
- MDN — @layer / cascade layers — how layers make ITCSS ordering a first-class, enforced feature.