Kodokon kodokon.com

CSS architecture: BEM, utility-first, file organization

Compare BEM and the utility-first approach, their real trade-offs in a team, and structure your files with cascade layers.

11 min · 3 questions

Open this lesson in Kodokon

CSS doesn't break at the scale of a component, it breaks at the scale of a project: specificity climbing just to "win" against an existing rule, fear of deleting code whose reach you don't know, styles leaking from one screen to another. A CSS architecture is above all a strategy for managing specificity and scope. The two dominant schools, BEM and utility-first, answer the same problem through opposite paths.

BEM (Block, Element, Modifier) names every styled node: .card (a standalone block), .card__title (an element belonging to the block), .card--featured (a variant of the block). The convention guarantees flat specificity - a single class everywhere, never a descendant selector - so no ordering conflicts, and a name that documents ownership. The cost: constantly inventing names, and genuine duplication of values (the same margin-top: 16px rewritten in twenty blocks).

CSS
.card {
  padding: 16px;
  border-radius: 8px;
}

.card__title {
  font-size: 1.25rem;
}

.card__title--muted {
  color: #6b7280;
}

.card--featured {
  border: 2px solid var(--brand);
}
BEM: flat specificity, explicit ownership

The utility-first approach (Tailwind being its dominant incarnation) flips the logic: no more names, just single-responsibility atomic classes composed in the HTML. The benefits are measurable: zero naming effort, styles colocated with the markup (deleting the component deletes its styles), and a final CSS bundle that stays almost constant in size since the utilities are shared. The trade-offs too: verbose HTML, duplication shifted to the markup that requires a component layer (React, Vue, partials) to stay DRY, and a learning curve for the vocabulary.

HTML
<article class="flex flex-col gap-4 rounded-lg
  border border-gray-200 p-4 shadow-sm">
  <h2 class="text-lg font-semibold">Invoice</h2>
  <p class="text-sm text-gray-600">
    Paid on March 12
  </p>
</article>
Utility-first: the styles live in the markup

These approaches are not mutually exclusive: many mature teams combine them - utilities for layout and spacing, component classes (BEM or CSS Modules) for complex patterns and states. As for file organization, the ITCSS principle remains the reference: order from generic to specific (reset, base elements, components, utilities). The native @layer rule turns this convention into an engine-level guarantee: priority between layers is independent of the specificity of the selectors they contain.

CSS
@layer reset, base, components, utilities;

@layer base {
  h2 { font-size: 1.5rem; }
}

@layer components {
  #hero .btn { padding: 12px 24px; }
}

@layer utilities {
  .p-0 { padding: 0; }
}
Cascade layers: order beats specificity

Knowledge check

Make sure you remember the key points of this lesson.

  1. In .card__title--muted, what does the --muted segment denote?
    • A CSS custom property
    • A modifier: a variant of the card__title element
    • A child element of card__title
    • A state managed by JavaScript
  2. What is the main contribution of @layer to the cascade?
    • It increases the specificity of the layer's selectors
    • It makes layer order take precedence over selector specificity
    • It isolates a component's styles the way a Shadow DOM would
    • It loads the layers in parallel to speed up rendering
  3. What trade-off do you accept when adopting a utility-first approach?
    • A CSS file that grows in proportion to the number of pages
    • High specificity that is hard to override
    • Verbose HTML whose deduplication relies on a component layer