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 likedangeracross Button, Tag, and Alert without duplicating CSS. -
Solution: BE.M keeps the Block and Element rules unchanged, but replaces the
--modifiersuffix 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>
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
dangeracross 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>
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>
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>
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);
}
}
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 { ... }
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);
}
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
}
}
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
}
}
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>
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
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;
}
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);
}
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));
}
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>
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);
}
<button class="ui-button featured">Upgrade Now</button>
<span class="ui-badge featured">New</span>
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;
}
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
!importantneeded 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"
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)