CSS · Architecture

CSS architecture (BEM, ITCSS)

Medium ⏱ 45m #architecture

← CSS

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--modifier means almost every selector is a single class — specificity 0,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 .card and 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 #id selector 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.
  • @layer inverts 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 !important are 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--modifier keeps almost every selector at a flat 0,1,0, so instead of escalating #id .class .class to 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 #id in an earlier one. I'd combine BEM for naming, layers for ordering, and reserve !important for the utilities layer only."

🔗 Go deeper

Primary source — the single best place to go deeper BEM
ESC

Type to search across every question bank.

navigate open 3,610 questions indexed