CSS at Scale

deep20 min

In one line

CSS at scale fails in one direction — it only ever grows — because nobody can prove a rule is unused, so the architecture's job is to make deletion safe.

What it is

The core problem is global scope plus no ownership. A selector might match something on a page nobody on the team has opened, so the safe action is always to add rather than remove. Multiply that by five years and you have a stylesheet where a third of the rules are dead and nobody dares find out which.

Every scoping strategy is really an answer to that. BEM gave conventional scoping through naming discipline. CSS Modules and CSS-in-JS made scope mechanical, so deleting a component deletes its styles. Utility CSS removed the problem by generating output only from what the source actually uses — the stylesheet stops growing with the codebase, which is Tailwind's most underrated property.

Cascade layers solve the other half: override order becomes explicit and declared instead of emerging from specificity accidents, so a new rule cannot quietly outrank an old one by being more specific.

For finding dead CSS, the honest position is that static analysis is unreliable — dynamic class names, framework-generated markup, and third-party widgets all defeat it. DevTools' Coverage panel measures actual usage on real pages and is a better starting point, and any deletion needs visual regression coverage behind it.

Critical CSS is the delivery-side concern: inline what the first viewport needs and load the rest asynchronously, so first paint does not wait on a full stylesheet. It is genuinely effective and genuinely awkward to maintain, since the critical set changes as the page does — automate the extraction or it goes stale.

Then the conventions that pay off regardless of tooling. Keep specificity flat and shallow. Put third-party CSS in an early layer. Use logical properties so RTL is free. Enforce token usage over raw values with a linter — Stylelint can fail a build on a hard-coded hex. Track CSS bundle size in CI alongside JavaScript. And write the visual regression tests that make deletion a reviewable diff rather than an act of faith.

Why it matters

Long-lived codebases accumulate CSS debt faster than JavaScript debt, precisely because the language offers no safe deletion, and it shows up as slow pages and inconsistent UI.

Interviewers at companies with a design system ask about this directly — "how do you keep CSS maintainable across teams?" — and the strong answer is about scoping, layers, and deletion safety rather than a naming convention.

Key points

  • CSS grows monotonically because global scope makes deletion unprovable — the architecture's job is safe deletion.
  • BEM conventionalised scope; CSS Modules and CSS-in-JS mechanised it; utilities eliminated the growth.
  • Cascade layers make override order explicit so specificity accidents cannot reorder your rules.
  • Static dead-code analysis is unreliable — use the Coverage panel plus visual regression before deleting.
  • Critical CSS improves first paint but goes stale; automate its extraction.
  • Lint for token usage over raw values, and track CSS size in CI.
  • Logical properties make RTL support a non-event.