CSS Architecture at Scale
The problem shape
Why stylesheets rot
Three forces compound over time. Specificity creep: each fix adds a more specific selector to beat the last one. Dead code: deleting a class feels risky because nothing tells you what still uses it. Location opacity: the rule that styles a button could be in any of six files, under any of four selectors. Every convention below attacks one of these.
Naming conventions
The idea behind BEM — and its variants
Block Element Modifier (BEM) is the most widespread naming contract: each class announces what it belongs to and what it is for, which makes the connection between markup and styles greppable and deletions safe.
/* Block: a standalone component */
.card { padding: 1rem; }
/* Element: a part of the block, named with __ */
.card__title { font-weight: 600; }
.card__body { color: var(--color-surface-ink); }
/* Modifier: a variant of the block or element, named with -- */
.card--featured { border-color: var(--color-action); }
.card__title--small { font-size: 0.875rem; }
- BEM (above): explicit, verbose, greppable. A solid default for teams.
- Utility-first: skip semantic classes; compose styles from many single-purpose classes in the markup (Tailwind's model — see the Ecosystem lessons).
- No convention: viable only with strict cascade discipline (layers + low-specificity selectors), typical for small design systems with deep component encapsulation.
The convention matters less than its two invariants: a reader can find where a style is defined, and a class can be deleted when its markup is gone. Pick one, write it down, enforce it in review.
Cascade layers as architecture
Ordering the codebase itself
@layer (introduced in the Cascade lesson) is the modern answer to specificity
creep. Declare the layer order once, early, and every file slots into a known priority:
/* The declaration order is the ONLY thing that sets layer priority.
Later @layer statements add files to these layers, never reorder them. */
@layer reset, tokens, base, layout, components, utilities;
@layer reset { *, *::before, *::after { box-sizing: border-box; } }
@layer tokens { :root { --space-3: 1rem; } } /* design tokens */
@layer base { body { line-height: 1.6; } } /* element defaults */
@layer layout { .page { display: grid; } } /* page shells */
@layer components { .card { padding: var(--space-3); } }
@layer utilities { .visually-hidden { /* ... */ } } /* highest: always win */
With layers in place, specificity only needs to rank rules within a layer — and utilities in
the top layer beat any component without a single !important. Unlayered overrides remain
available for one-off patches, deliberately ranked above everything.
The specificity budget
Layering replaces the older, weaker guardrails — but the habits still help: keep selectors to one class
where possible, avoid IDs in selectors entirely, and reserve !important for the utilities
layer. If a fix needs two classes and a parent chain to win, the fix belongs in a higher layer, not in a
longer selector.
Organizing files
A file layout that scales
/* styles.css - the entry point: order IS the layer contract */
@layer reset, tokens, base, layout, components, utilities;
@import url("reset.css") layer(reset);
@import url("tokens.css") layer(tokens);
@import url("base.css") layer(base);
@import url("layout.css") layer(layout);
/* components: one file per component, imported where used */
@import url("components/card.css") layer(components);
@import url("utilities.css") layer(utilities);
The layer() import annotation assigns a file to a layer at import time, so a bundler-less
project still gets deterministic ordering. Within components/, one file per component keeps
deletions local: delete the component, delete its file.
Deleting dead CSS safely
Workflow that works: grep the class name across the templates and scripts (not just HTML — class names
are constructed in JavaScript too), remove the rule, run the visual smoke test, commit the deletion
separately. Tooling (Stylelint's no-unused-style plugins, coverage tools) helps at scale —
see References.
Practice: architect a stylesheet
The task
Take a multi-page project you have styled and reorganize it into the layered structure above, one step at a time, verifying visually after each step.
The checklist
- Declare the layer order once and move the reset into
layer(reset). - Rename one component's classes to a BEM contract and confirm no other rule needed changing.
- Delete one dead rule end-to-end: grep, delete, smoke test, commit.
- Move an
!importantfix into the utilities layer and remove the importance.