DEV Community

Cover image for From BEM to BE.M — A CSS Naming Convention Built for Component Libraries
Lane Zhao
Lane Zhao

Posted on Originally published at linkedin.com

From BEM to BE.M — A CSS Naming Convention Built for Component Libraries

Originally published on LinkedIn. I'm gradually migrating my LinkedIn articles over to DEV — this is the first one.

BEM was designed for page-level stylesheets. Component libraries need something more. BE.M drops the double-dash modifier prefix, unlocks cross-component modifier sharing, and uses CSS custom properties to make theming feel effortless — with zero breaking changes to your mental model.

TL;DR

  • Problem: Classic BEM modifiers repeat the block name on every variant (.ui-button--danger, .ui-button--large), making HTML noisy and making it impossible to share a modifier like danger across Button, Tag, and Alert without duplicating CSS.
  • Solution: BE.M keeps the Block and Element rules unchanged, but replaces the --modifier suffix with a chained standalone class on the block root: class="ui-button danger".
  • Power move: Pair the chained modifier with CSS custom properties. A single global .danger { --color: ... } rule cascades into every component that participates — one definition, zero duplication.
  • When to use it: Global SCSS in a UI or component library. Not compatible with CSS Modules.

The Problem With Classic BEM in a Component Library

BEM (Block–Element–Modifier) is one of the most widely adopted CSS naming conventions, and for good reason: it brings structure, clarity, and predictable specificity to large-scale stylesheets. But after building and maintaining a UI component library, a recurring pain pattern is hard to ignore.

Look at a typical BEM button with multiple modifiers:

<button class="ui-button ui-button--danger ui-button--large ui-button--outlined">Delete</button>
Enter fullscreen mode Exit fullscreen mode

The block name ui-button appears four times on a single element. That's not just visual noise — it's a structural problem baked into how BEM encodes modifier state.

The friction compounds in a component library:

  • Every modifier has to duplicate the full block namespace.
  • Adding a new visual variant means writing modifier CSS separately for each component.
  • There's no natural path to share a modifier like danger across Button, Tag, Alert, and Badge without repeating the same CSS logic in four places.

BEM was designed for large web page stylesheets. Reusable component libraries have different constraints: modifiers are more likely to carry semantic intent than layout instructions, and that intent should travel across component boundaries.

Introducing BE.M — Block, Element, Chained Modifier

The core insight is simple: a modifier doesn't need to carry the block prefix. It just needs to be scoped to the element it's applied on. CSS compound selectors already do exactly that.

The Three Rules

Block — a top-level component, namespaced with a library prefix:

<button class="ui-button">...</button>
<span class="ui-tag">...</span>
<div class="ui-alert">...</div>
Enter fullscreen mode Exit fullscreen mode

Element — a component's internal part, joined with __ (double underscore):

<button class="ui-button">
  <span class="ui-button__label">Submit</span>
  <svg class="ui-button__icon">...</svg>
</button>
Enter fullscreen mode Exit fullscreen mode

Modifier — a short, standalone class chained directly on the block root. No -- suffix, no block prefix repetition.

<!-- modifier on the block root -->
<button class="ui-button danger">Delete</button>
<button class="ui-button large outlined">Submit</button>

<!-- don't attach modifiers to elements -->
<span class="ui-button__label muted">Cancel</span>
Enter fullscreen mode Exit fullscreen mode

The CSS for this becomes a compound selector, always nested inside the block:

.ui-button {
  &.danger {
    /* reassign block-level CSS variables */
  }
  &.large {
    /* reassign block-level CSS variables */
  }
  &.outlined {
    /* reassign block-level CSS variables */
  }

  // child elements respond through variables, never through their own modifier classes
  &__label {
    color: var(--button-label-color);
  }
  &__icon {
    fill: var(--button-icon-color);
  }
}
Enter fullscreen mode Exit fullscreen mode

Compare that to classic BEM, where every modifier has to repeat the block name:

// classic BEM — block name duplicated in every modifier
.ui-button--danger   { ... }
.ui-button--large    { ... }
.ui-button--outlined { ... }
Enter fullscreen mode Exit fullscreen mode

The compound selector .ui-button.danger fires only when both classes appear on the same element — the same scoping guarantee as BEM, without the name repetition.

Designing a Block's CSS Variable Model

CSS custom properties aren't just a companion to BE.M — they're the mechanism that makes it scale. To use the pattern well, rethink the block root: it's not only the component's container, it's the component's variable scope.

Start by Mapping What Modifiers Change

Before writing any SCSS, list every modifier the block will support and map out which CSS properties each one affects:

Modifier Properties affected
danger border color, background, title color
featured background, box-shadow
compact padding, font-size

This table tells you exactly which properties to extract as CSS variables — and which to leave as plain values.

Step 1 — Define Variables on the Block Root

Only extract properties that modifiers will need to override. Resist the urge to variablize everything:

.ui-card {
  // declare only what modifiers will change
  --card-border-color: var(--color-border);
  --card-bg: var(--color-surface);
  --card-title-color: var(--color-text-primary);
  --card-shadow: var(--shadow-sm);
  --card-padding: var(--space-4);
  --card-font-size: var(--text-base);

  // wire actual CSS properties to those variables
  border: 1px solid var(--card-border-color);
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
  padding: var(--card-padding);
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Elements Consume Variables Only Where Needed

Child elements use variables for properties that modifiers affect; everything else stays as a plain value:

.ui-card {
  &__title {
    color: var(--card-title-color);   // modifier changes this → use variable
    font-size: var(--card-font-size); // modifier changes this → use variable
    font-weight: 600;                 // never changes → plain value
    margin-bottom: 8px;               // never changes → plain value
  }

  &__body {
    font-size: var(--card-font-size); // modifier changes this → use variable
    line-height: 1.6;                 // never changes → plain value
  }

  &__footer {
    border-top: 1px solid var(--card-border-color); // modifier changes this → use variable
    padding-top: 8px;                               // never changes → plain value
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Modifiers Only Reassign Block Variables

A modifier's entire job is to reassign the variables declared on the block root. It never reaches into child elements directly:

.ui-card {
  &.danger {
    --card-border-color: var(--color-error);
    --card-bg: var(--color-error-bg);
    --card-title-color: var(--color-error-dark);
    // shadow and padding are unchanged — no override needed
  }

  &.featured {
    --card-bg: var(--color-brand-subtle);
    --card-shadow: var(--shadow-lg);
    // colors and font-size are unchanged — no override needed
  }

  &.compact {
    --card-padding: var(--space-2);
    --card-font-size: var(--text-sm);
    // colors are unchanged — no override needed
  }
}
Enter fullscreen mode Exit fullscreen mode

When a modifier only reassigns root-level variables, the entire element tree responds automatically — no conditional branching, no specificity fights:

<!-- add .danger once at the root; all children respond automatically -->
<div class="ui-card danger">
  <h3 class="ui-card__title">...</h3>  <!-- picks up error color -->
  <p class="ui-card__body">...</p>     <!-- font-size unchanged -->
  <footer class="ui-card__footer">...  <!-- border follows error color -->
</div>
Enter fullscreen mode Exit fullscreen mode

Variable Naming Convention

Use --{block}-{property} to make scope obvious and prevent collisions with global design tokens:

// block-scoped — no ambiguity
--card-border-color
--button-label-color
--tag-background

// too generic — at risk of being accidentally overridden
--border-color
--color
Enter fullscreen mode Exit fullscreen mode

The Real Win: Shareable Semantic Modifiers

This is where BE.M pays off at scale.

In classic BEM, giving Button, Tag, and Alert a shared danger state means writing three separate modifier blocks, each with its own hardcoded color values:

// classic BEM — same semantic intent, written three times
.ui-button--danger {
  background-color: #e53e3e;
  border-color: #e53e3e;
  color: white;
}
.ui-tag--danger {
  background-color: #fff5f5;
  color: #e53e3e;
  border-color: #fc8181;
}
.ui-alert--danger {
  background-color: #fff5f5;
  color: #e53e3e;
}
Enter fullscreen mode Exit fullscreen mode

The same intent lives in three places, encoded with slightly different values. Over time those values diverge. Design reviews flag the inconsistency. Someone patches one, misses the others.

With BE.M and CSS custom properties, the modifier becomes a semantic signal, not a hardcoded style:

// defined once, globally — semantic intent as a variable override
.danger {
  --color: var(--color-error);
  --hover-color: var(--color-error-hover);
  --background-color: var(--color-error-bg);
}

.safe {
  --color: var(--color-success);
  --hover-color: var(--color-success-hover);
  --background-color: var(--color-success-bg);
}

.mute {
  --color: var(--color-muted);
}
Enter fullscreen mode Exit fullscreen mode

Each component opts into those shared tokens through its own internal variable fallback chain:

.ui-button {
  // reads own token first, falls back to shared modifier token, then a hard default
  color: var(--button-color, var(--color, currentColor));
  background-color: var(--button-bg, var(--background-color, transparent));
}

.ui-tag {
  color: var(--tag-color, var(--color, currentColor));
  background-color: var(--tag-bg, var(--background-color, transparent));
}

.ui-alert {
  border-color: var(--alert-border-color, var(--color, currentColor));
  background: var(--alert-bg, var(--background-color, transparent));
}
Enter fullscreen mode Exit fullscreen mode

Now a single modifier class covers all three with zero additional CSS:

<button class="ui-button danger">Delete</button>
<span class="ui-tag danger">Critical</span>
<div class="ui-alert danger">Something went wrong.</div>
Enter fullscreen mode Exit fullscreen mode

Compounding Architectural Benefits

The shared modifier pattern has benefits beyond cutting duplicate CSS:

Visual consistency by default. danger always means the same color intent everywhere it's used — no risk of Button's red drifting away from Tag's red over a series of patches.

Dramatically reduced CSS surface area. A design system with 30 components and 6 semantic modifiers would need 180 modifier blocks in classic BEM. With shared BE.M modifiers, you write 6.

Consumer-side extensibility. Application teams can define their own modifiers without touching the component library source:

// in the application stylesheet — no library fork required
.featured {
  --color: var(--brand-gold);
  --background-color: var(--brand-gold-bg);
}
Enter fullscreen mode Exit fullscreen mode
<button class="ui-button featured">Upgrade Now</button>
<span class="ui-badge featured">New</span>
Enter fullscreen mode Exit fullscreen mode

Subtree theming. Because modifiers cascade through CSS variables, you can scope a theme to a region of the page without touching component CSS:

.dark-section {
  --color-error: #fc8181;
  --color-error-bg: #1a1a1a;
}
Enter fullscreen mode Exit fullscreen mode

Every danger element inside .dark-section automatically adopts the adjusted palette.

Specificity and Performance — Addressing the Common Objections

Specificity Is Higher — and That's a Feature

In classic BEM, all modifier selectors have a specificity of (0-1-0) — a single class. In BE.M, the compound selector .ui-button.danger is (0-2-0). This is actually an advantage:

  • Modifiers reliably override base block styles without relying on source order.
  • Every modifier has consistent specificity (0-2-0), keeping the cascade predictable.
  • No !important needed to force a modifier to apply.

The Performance Concern Is Overblown

When compound selectors come up, CSS performance concerns tend to follow. The concern has a technical basis, but it's disproportionate in component library usage.

Browsers match selectors right to left:

.ui-button.danger
           ↑
           1. Find all elements with class "danger"
              2. Check whether each also has class "ui-button"
Enter fullscreen mode Exit fullscreen mode

Compared to .ui-button--danger, BE.M's compound selector adds one extra class-membership check. In practice, that's a bitwise in-memory operation that takes nanoseconds — on only the subset of elements that carry .danger.

The Chrome team's own performance docs put it plainly:

Selector complexity only becomes measurable in extreme conditions: 10,000+ DOM nodes with frequent full-tree style recalculations, deeply nested 5+ level selector chains, or per-frame mass DOM insertions in a 60 fps animation loop.

None of those apply to a component library. If your app has CSS performance problems, the fix is reducing unnecessary reflows, using will-change to isolate compositing layers, or profiling JavaScript. Reverting .ui-button.danger to .ui-button--danger won't move the needle.

Trade-offs to Know Before You Commit

BE.M's strengths come with three real constraints worth naming before adoption:

Modifier name collisions. Short names like active, disabled, and danger are common in general stylesheets. A host app that defines .danger { color: red } could conflict with a component modifier. The CSS variable approach largely neutralizes this — .danger { color: red } does not affect a component reading --color from its internal token fallback chain — but it's something to document and communicate.

CSS Modules incompatibility. CSS Modules hashes class names, turning .danger into .danger_abc123. The global shared modifier pattern breaks entirely. If your project uses — or plans to use — CSS Modules, BE.M is not a fit.

Lower global searchability. ui-button--danger is unambiguous in a codebase-wide search. A bare danger class could mean anything. Good naming discipline and documentation matter more here than in classic BEM.

When to Use BE.M

Context Recommendation
Building a UI / component library ✅ Strong fit
Global SCSS (no CSS Modules) ✅ Required
CSS custom property token system ✅ Best pairing
Large page stylesheet (no components) ⚠️ Classic BEM is simpler here
CSS Modules / scoped styles ❌ Incompatible — use a different approach

Conclusion

BEM gave CSS at scale its first reliable structure, and that contribution is lasting. But its modifier encoding — duplicating the block prefix on every variant — was always a tradeoff of clarity for verbosity. In a component library, where modifiers carry semantic intent that needs to travel across block boundaries, that tradeoff compounds.

BE.M separates the concerns properly: blocks and elements own structure; modifiers own semantic intent. When that intent is expressed through CSS custom properties rather than hardcoded values, modifiers become a lightweight theming API that any component can opt into with a single fallback chain.

The class names get shorter. The HTML gets cleaner. The design system stays consistent. That's worth moving on for.


Notes

  • Original version: From BEM to BE.M on LinkedIn. I'm slowly migrating my LinkedIn articles here, one at a time.
  • AI disclosure: I used AI assistance to polish the English and tighten the structure of this piece. The ideas are my own, and the convention described here has been running in production in component libraries I've built and maintained for over two years.
  • Feedback welcome: especially from anyone who has tried chained modifiers or a similar approach in a design system — tell me where it broke for you.

Top comments (0)