Lesson 24 of 25

CSS Architecture & Organization

CSS Is Global, and That Is the Problem

Every rule you write applies to the entire document. There is no built-in scoping, no module boundary, nothing that stops a rule written for the pricing page from reaching into the footer on the contact page. On a hundred-line stylesheet that freedom is convenient. On a project with fifty components and three people editing it, it is the reason nobody dares delete anything.

CSS architecture is the name for the conventions a team adopts to impose the boundaries the language does not provide. None of it is enforced by the browser — a methodology is an agreement about how you will name things and where you will put them, and it works exactly as well as your consistency in following it.

Every approach in wide use is trying to answer the same three questions. What do I call this class, so that its name tells me where it belongs and what it affects? Where does this rule live, so that someone else can find it? And in what order do the rules load, so that overriding behaves predictably instead of by accident?

You do not need a large framework to get most of the benefit. A naming convention, a consistent file order, and the discipline of keeping selectors shallow will carry a student project or a small business site comfortably.

  • There is no scoping in plain CSS — every rule is global
  • A methodology is a convention, not a browser feature
  • Naming answers "what does this affect?"
  • File organisation answers "where do I find it?"
  • Load order answers "what wins?"
  • Consistency matters more than which methodology you choose

BEM: Block, Element, Modifier

BEM is the most widely used naming convention, and its whole idea fits in three terms. A block is a standalone component with a meaningful name: card, navbar, search-form. An element is a part of that block that has no meaning outside it, written with two underscores: card__title, card__price. A modifier is a variant of a block or element, written with two hyphens: card--featured, btn--large.

The benefit is not really the punctuation. It is that every rule ends up as a single class. .card__title has exactly the same specificity as .card and as .btn--large, so nothing ever out-ranks anything else and the cascade is decided by file order, which you control. All the specificity problems from the previous lesson simply stop occurring.

The second benefit is that the class name tells you where the CSS lives. Seeing card__footer in the HTML, you know to open the card component and you know that changing it cannot affect anything outside a card. That is the scoping the language does not give you, recreated by convention.

One point is regularly misunderstood: the element part does not describe nesting depth. Even if the price is three levels deep inside the card's markup, it is still card__price and never card__body__price. Names get long, hard to read and fragile that way. And if a part starts to make sense on its own — if you want to use it outside the card — that is the signal it should become its own block instead.

Example
<article class="card card--featured">
  <img class="card__image" src="book.jpg" alt="">
  <div class="card__body">
    <h3 class="card__title">CSS Fundamentals</h3>
    <p class="card__price card__price--sale">299</p>
    <button class="btn btn--primary">Add to cart</button>
  </div>
</article>

/* Block */
.card { background: var(--surface); border-radius: var(--radius); }

/* Elements — one class each, all the same specificity */
.card__image { width: 100%; display: block; }
.card__body  { padding: 20px; }
.card__title { font-size: 1.1rem; margin: 0 0 8px; }
.card__price { font-weight: 700; }

/* Modifiers */
.card--featured      { border: 2px solid var(--brand); }
.card__price--sale   { color: var(--brand); }

/* .btn is its own block — it is used outside cards too */
.btn          { padding: 10px 18px; border-radius: var(--radius); }
.btn--primary { background: var(--brand); color: #ffffff; }

/* Wrong: depth does not belong in the name */
.card__body__title { }
Notes
  • SMACSS and ITCSS are the other two names you will meet. SMACSS sorts rules into Base, Layout, Module, State and Theme. ITCSS orders the whole stylesheet so that specificity only ever increases as you read down the file. Both aim at the same targets as BEM from a different angle, and BEM's naming works comfortably inside either.

Layering the Stylesheet in the Right Order

Since two rules of equal specificity are decided by source order, the sequence in which your CSS loads is part of the design. The convention that works nearly everywhere is four layers, loaded in this order.

Base comes first: a reset or normalisation, box-sizing, the custom properties, and bare element styles for body, headings, links and lists. These are the defaults everything else builds on, and they should be the easiest thing in the file to override. Layout comes next: the page shell, containers, grids, the header and footer skeleton — the arrangement of large regions, without any component detail.

Components is the biggest layer and the one you will spend most time in: cards, buttons, forms, badges, modals, each self-contained. Utilities comes last, deliberately, so that a single-purpose helper such as .is-hidden or .text-center can win against a component rule without needing !important. Loading utilities first is a common mistake and it is precisely what forces people to reach for !important.

Modern CSS can enforce this ordering explicitly with cascade layers. Declaring @layer base, layout, components, utilities; at the top of your stylesheet fixes the priority between the layers regardless of specificity — a single-class rule in the utilities layer beats a four-class rule in the components layer, because layer order is considered before specificity. It also means a low-specificity reset can never accidentally be beaten into submission by an over-specific component rule. Anything left outside a layer wins over everything inside one, which is worth remembering when you mix the two.

Example
/* Declare the order once, at the very top */
@layer base, layout, components, utilities;

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  :root { --brand: #d1039e; --radius: 10px; }
  body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.6; }
}

@layer layout {
  .container { max-width: 1100px; margin-inline: auto; padding-inline: 20px; }
  .page { display: grid; grid-template-columns: 260px 1fr; gap: 32px; }
}

@layer components {
  .card { background: #fff; border-radius: var(--radius); padding: 20px; }
  .card__title { font-size: 1.1rem; }
}

@layer utilities {
  /* Beats the components layer without !important */
  .is-hidden    { display: none; }
  .text-center  { text-align: center; }
}
  • Base — reset, custom properties, bare element styles
  • Layout — containers, the page grid, header and footer skeleton
  • Components — cards, buttons, forms, everything reusable
  • Utilities — single-purpose helpers, loaded last so they win
  • @layer makes the order explicit instead of depending on file sequence
  • Layer order is considered before specificity; unlayered rules beat layered ones

Files, Utilities, and Choosing What Fits the Project

Split the stylesheet along the same lines as the layers, one file per component, named after the block it contains — card.css holds .card and everything beginning card__. The rule to hold to is that a class name should tell you the filename. When somebody reports that the card footer is wrong, nobody has to search.

Those files are combined for delivery. A build tool bundles and minifies them into one request; without one, use several <link> tags, which download in parallel. Avoid chains of @import inside CSS files, for the reason covered in the previous lesson: each import is only discovered after its parent has been parsed.

Utility classes are worth a deliberate decision rather than a drift. A small set is genuinely useful — visibility helpers, text alignment, perhaps a spacing scale — and they belong in the last layer. At the other extreme, utility-first frameworks build entire interfaces from them, moving styling decisions into the HTML; that is a coherent approach with real advantages, but it is a whole-project commitment, not something to mix in halfway. What causes trouble is the middle ground, where half the styles are components and half are utilities and nobody knows which to reach for.

Finally, match the ceremony to the project. A three-page site does not need cascade layers and a build pipeline; consistent BEM naming in one well-ordered file is plenty. A large application with several contributors needs the structure, because the cost of everybody guessing is much higher than the cost of the convention. The failure mode to avoid is adopting a methodology and then abandoning it halfway, which leaves a codebase with two competing systems and the benefits of neither.

Example
/* A layout that scales without ceremony */
/*
styles/
├── base/
│   ├── reset.css
│   ├── variables.css
│   └── typography.css
├── layout/
│   ├── container.css
│   ├── header.css
│   └── footer.css
├── components/
│   ├── button.css
│   ├── card.css
│   ├── form.css
│   └── modal.css
└── utilities/
    └── helpers.css
*/

/* A small, honest set of utilities */
@layer utilities {
  .is-hidden { display: none; }

  /* Hidden visually, still read by screen readers */
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .text-center { text-align: center; }
  .stack-sm > * + * { margin-top: 8px; }
  .stack-md > * + * { margin-top: 16px; }
}
Notes
  • .visually-hidden is worth copying into every project. It hides content from sight while keeping it available to screen readers — the correct way to give an icon-only button a label, or to add context to a "Read more" link that would otherwise be meaningless when read out of context.
Ask AI