The Scalability Problem in CSS

Building and maintaining a large CSS codebase is one of the hardest challenges in front-end development. Every approach aims to balance two goals: efficiency (spend less time deciding how to do things, more time doing them) and consistency (ensure every developer on a team follows the same patterns). This is a report on one working method, developed while building a component library with 220+ reusable patterns: combining CSS globals, BEM naming, and utility classes.

Core Concepts in Brief

CSS Globals are stylesheets that define cross-cutting rules—colors, a typographic scale, spacing units. In practice, they act as tokens for design consistency. For example, typography rules can live in a single global file:

/* Typography | Global */
:root {
  /* body font size */
  --text-base-size: 1em;


  /* type scale */
  --text-scale-ratio: 1.2;
  --text-xs: calc((--text-base-size / var(--text-scale-ratio)) / var(--text-scale-ratio));
  --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
  --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
  --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
  --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
  --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
}


@media (min-width: 64rem) { /* responsive decision applied to all text elements */
  :root {
    --text-base-size: 1.25em;
    --text-scale-ratio: 1.25;
  }
}


h1, .text-xxl   { font-size: var(--text-xxl, 2.074em); }
h2, .text-xl    { font-size: var(--text-xl, 1.728em); }
h3, .text-lg    { font-size: var(--text-lg, 1.44em); }
h4, .text-md    { font-size: var(--text-md, 1.2em); }
.text-base      { font-size: --text-base-size; }
small, .text-sm { font-size: var(--text-sm, 0.833em); }
.text-xs        { font-size: var(--text-xs, 0.694em); }

BEM (Block, Element, Modifier) is a naming methodology for creating scoped, reusable components. A block is a standalone component (e.g., .card), an element is a child of a block (e.g., .card__title), and a modifier is a variation (e.g., .card--highlighted).

Utility classes handle single responsibility styling—like .text-lg for font size or .padding-md for spacing:

<section class="padding-md">
  <h1>Title</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</section>


<style>
  .padding-sm { padding: 0.75em; }
  .padding-md { padding: 1.25em; }
  .padding-lg { padding: 2em; }
</style>

These tools aren't used in isolation. Utility classes can connect to global token scales (rather than hard-coded values), and entire basic components can be built from them:

/* Spacing | Global */
:root {
  --space-unit: 1em;
  --space-xs:   calc(0.5 * var(--space-unit));
  --space-sm:   calc(0.75 * var(--space-unit));
  --space-md:   calc(1.25 * var(--space-unit));
  --space-lg:   calc(2 * var(--space-unit));
  --space-xl:   calc(3.25 * var(--space-unit));
}

/* responsive rule affecting all spacing variables */
@media (min-width: 64rem) {
  :root {
    --space-unit:  1.25em; /* 👇 this responsive decision affects all margins and paddings */
  }
}

/* margin and padding util classes - apply spacing variables */
.margin-xs { margin: var(--space-xs); }
.margin-sm { margin: var(--space-sm); }
.margin-md { margin: var(--space-md); }
.margin-lg { margin: var(--space-lg); }
.margin-xl { margin: var(--space-xl); }

.padding-xs { padding: var(--space-xs); }
.padding-sm { padding: var(--space-sm); }
.padding-md { padding: var(--space-md); }
.padding-lg { padding: var(--space-lg); }
.padding-xl { padding: var(--space-xl); }
<article class="padding-md bg radius-md shadow-md">
  <h1 class="text-lg color-contrast-higher">Title</h1>
  <p class="text-sm color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</article>

When BEM Alone Falls Short

To stress-test these approaches, consider building a gallery of card components. Using a BEM-only approach gives clear advantages: thanks to its flat structure, you get low CSS specificity and strong component scope. You avoid messy inheritance rules like .card > a that can overwrite styles unpredictably across a project:

/* without BEM */
.grid {}
.card {}
.card > a {}
.card img {}
.card-content {}
.card .title {}
.card .description {}


/* with BEM */
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title {}
.card__description {}
/* without BEM */
.card > a.active {} /* high specificity */


/* without BEM, when things go really bad */
div.container main .card.is-featured > a.active {} /* good luck with that 😦 */


/* with BEM */
.card__link--active {} /* low specificity */

However, pure BEM projects typically run into two concrete obstacles.

First, naming is heavy. Naming dozens of new blocks elements is mentally tiring. Creating a smaller paragraph inside a card might lead any of your teammates to choose a different, arbitrarily "correct" name—.card__description--small, a new .card__small class, or even something entirely out-of-sync like .card__sub-note. The cost isn't just writing these; it's all devs on a project agreeing and consistently remembering them.

Second, small variations lead to class sprawl. Say you need a card with centered text. You create .card--center. A teammate on an another component (e.g., a banner) faces the same need and writes a specific .banner--center variation. Later, you need a centered banner. On instinct, you might write banner banner--center in your HTML, expecting it to work because you're using the same naming convention as with the card. You're wrong. Now you have to open the banner's heavy CSS file, inspect it, and hunt for the relevant class. Minute-fraction lost, repeated hundreds of times across a team equals hours of unnecessary overhead and unnecessary CSS bloat.

Reducing Scope with Globals

One major payoff of a global token scale is handling responsive behavior upfront. Instead of writing a crop of media queries, global rules can adjust base properties and, since sizing values are derived from a single source (e.g., em units based on a root font-size), a cascade effect occurs. When you modify a global value at a specific breakpoint, the whole system of margins, paddings, and typography shifts in unison:

Spacing and typography responsive rules — no media queries on a component level 
/* without globals */
.card { padding: 1em; }


@media (min-width: 48rem) {
  .card { padding: 2em; }
  .card__content { font-size: 1.25em; }
}


/* with globals (responsive rules intrinsically applied) */
.card { padding: var(--space-md); }

Global patterns can also replace component-level logic. A common pattern like a "text wrapper" can be defined as a global behavioral class that handles line-height, vertical rhythm, and base text styles. Reusing this class (.text-component) in your card's content area instantly standardizes the typography across every text container in a template—reducing the CSS footprint from your component files.

Introducing Utilities for Customization

This is where utility classes earn their keep. By swapping specific element styles for generic utilities, the card's markup slims down and its customization potential grows:

<article class="card radius-lg">
  <a href="#0" class="block color-inherit text-decoration-none">
    <figure>
      <img class="block width-100%" src="image.jpg" alt="Image description">
    </figure>


    <div class="text-component padding-md">
      <h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
      <p class="color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
    </div>


    <div class="card__icon-wrapper" aria-hidden="true">
      <svg class="icon icon--sm color-white" viewBox="0 0 24 24"><!-- icon --></svg>
    </div>
  </a>
</article>
.card {}
.card__title {}
.card__icon-wrapper {}

This change leaves the BEM class count dramatically lower—from 9 classes down to just 3 core, non-negotiables. The rest of the visual treatment (like sizing, colors, and alignment) is handled via predictable utilities in the HTML. Adjusting for a bigger card title is now an effortless, "readable" change: swapping text-lg for a global text-xl directly. Centering the card's text is just adding the text-center utility to that text wrapper. No checking CSS, no new class names invented, no risk of clashing scopes.

Why Pure Utilities Don't Win Either

Given these obvious benefits, it's tempting to conclude BEM is redundant. Yet two issues strongly justify a hybrid approach.

Readability and structure suffer. HTML laced with only dozens of utility classes becomes painful to interpret at a glance. Using BEM is ideal for the parts of a component that don’t visually change from one instance to the next—such as layout scaffolding, complex state/behavior (transitions, hover effects, advanced animations), or highly synergistic grouping of unrelated elements within a complex card or banner. Alternatives exist for simple tasks like adding a positioning context to a wrapper: a simple position-relative utility is perfectly fine:

<!-- use only Utility classes -->
<article class="position-relative overflow-hidden bg radius-lg transition-all duration-300 hover:shadow-md col-6@sm col-4@md">
  <!-- card content -->
</article>


<!-- use BEM + Utility classes -->
<article class="card radius-lg col-6@sm col-4@md">
  <!-- card content -->
</article>

Note that you don't need !important to make utilities work. The goal isn't forcing style overrides; it's to only leverage them for straightforward visual shifts or properties that are painful to build a semantic class for. The decision of which method to use for a style doesn't need to be perfect on the first pass—it can be refactored later between BEM and utilities depending on emerging maintenance needs.

Complex effects need actual CSS. Animated iconography, hover motion with curated bezier() curves, or stylistic pseudo-elements are impractical and difficult to police when mixed into utility-only markup. Their logic is best contained in a dedicated BEM component with keyframes defined in a stylesheet that allows precise, bespoke tuning.

Architecture in Practice

The bookends of this method are clear file organization. Each component gets its own, dedicated CSS file:

project/
└── main/
    ├── assets/
    │   ├── css/
    │   │   ├── components/
    │   │   │   ├── _card.scss
    │   │   │   ├── _footer.scss
    │   │   │   └── _header.scss
    │   │   ├── globals/
    │   │   │   ├── _accessibility.scss
    │   │   │   ├── _breakpoints.scss
    │   │   │   ├── _buttons.scss
    │   │   │   ├── _colors.scss
    │   │   │   ├── _forms.scss
    │   │   │   ├── _grid-layout.scss
    │   │   │   ├── _icons.scss
    │   │   │   ├── _reset.scss
    │   │   │   ├── _spacing.scss
    │   │   │   ├── _typography.scss
    │   │   │   ├── _util.scss
    │   │   │   ├── _visibility.scss
    │   │   │   └── _z-index.scss
    │   │   ├── _globals.scss
    │   │   ├── style.css
    │   │   └── style.scss
    │   └── js/
    │       ├── components/
    │       │   └── _header.js
    │       └── util.js
    └── index.html

Each of those component files houses only the "behavioral" BEM CSS those states call for. Across them sits the shared global token set. There's no prescriptive single configuration—organize your globals as either separate files or a monolithic globals.css. The final card gallery in this method demonstrates the flexibility in full effect, where BEM handles the heavy machinery and utility classes take care of the on-the-fly responsive graph challenges.

The Final Verdict

Large-scale, maintainable CSS can't rely on a single set of rules. Rigid BEM blocks waste time on redundant naming and minor visual variations. Utility-class-only projects lose semantic clarity and choke on complex motion design. This architecture is a middle path: global design tokens foster radical consistency and adaptive resizing, a thin BEM layer handles component micro-structure and behavioral complexity, and expressive utility classes are reserved for rapid, effortless, and customizable visual tweaks. This setup reduces cognitive friction and extra CSS while making long-term development more predictable.