
BEM Naming Convention: Writing Maintainable CSS Class Names
Naming things is famously one of the hard problems in programming, and CSS makes it harder than most languages. Every class name lives in one global namespace, selectors can accidentally match elements in completely unrelated parts of a page, and six months later nobody remembers whether .title is safe to change. BEM is a naming convention that tackles this head-on. It gives every class a predictable structure that tells you what it styles, where it belongs, and what variation it represents.
In this guide, I'll explain the BEM methodology, show how to apply it to real components, and cover the mistakes I see most often when teams adopt it.
What BEM Stands For
BEM stands for Block, Element, Modifier. It was developed at Yandex and has become one of the most widely used CSS naming conventions.
- A block is a standalone, reusable component:
.card,.nav,.search-form. - An element is a part of a block that has no meaning on its own:
.card__title,.nav__link. - A modifier is a flag that changes a block's or element's appearance or behavior:
.card--featured,.nav__link--active.
The syntax most teams use:
.block
.block__element
.block--modifier
.block__element--modifier
Two underscores separate a block from its element, and two hyphens separate a modifier. Single hyphens are used within names: .search-form__submit-button.
A First Example
Here's a simple card component, first without BEM:
<div class="card featured">
<img class="image" src="/images/lighthouse.jpg" alt="Lighthouse at sunset" />
<div class="content">
<h3 class="title">Coastal Walks</h3>
<p class="text">Five routes along the northern cliffs.</p>
<a class="button" href="/walks">Read more</a>
</div>
</div>
.card .title {
font-size: 1.25rem;
}
.card .button {
background: #0f766e;
}
.featured {
border: 2px solid #f59e0b;
}
Class names like .title, .image, and .button are almost certain to collide with other components. .featured could mean anything. And the descendant selectors create specificity that's harder to override later.
Now with BEM:
<article class="card card--featured">
<img
class="card__image"
src="/images/lighthouse.jpg"
alt="Lighthouse at sunset"
/>
<div class="card__body">
<h3 class="card__title">Coastal Walks</h3>
<p class="card__text">Five routes along the northern cliffs.</p>
<a class="card__link" href="/walks">Read more</a>
</div>
</article>
.card {
display: grid;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
}
.card__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card__body {
padding: 1.25rem;
}
.card__title {
margin: 0 0 0.5rem;
font-size: 1.25rem;
}
.card__text {
margin: 0 0 1rem;
color: #475569;
}
.card__link {
font-weight: 600;
color: #0f766e;
}
.card--featured {
border: 2px solid #f59e0b;
}
Every selector is a single class. Every class name says which component it belongs to. You can search the codebase for card__ and find every piece of the card.
Why BEM Works
Flat, Low Specificity
Because every rule targets one class, all your selectors have the same specificity (0,1,0). Rules later in the file win, and there's no creeping escalation of nested selectors or !important to override something. That predictability is BEM's most underrated benefit.
No Accidental Collisions
.card__title will never clash with .modal__title. You can drop components into any page without worrying about stray styles leaking in or out.
Self-Documenting Markup
Reading the HTML tells you the structure. class="pricing__plan pricing__plan--highlighted" says "this is a plan inside the pricing block, and it's the highlighted variation" without opening the stylesheet.
Safe Refactoring
If you want to change .card__title, you know it only affects cards. Delete a component and you can delete every class with its prefix. No guessing about whether some other page depends on it.
Blocks in Depth
A block should make sense on its own and be movable anywhere on the page. Good block names describe what something is, not what it looks like:
Good: .product-tile, .site-header, .newsletter-form
Avoid: .blue-box, .left-column, .big-text
Blocks can nest inside other blocks. A .card can contain a .button block and an .avatar block. Each keeps its own namespace:
<article class="card">
<div class="card__footer">
<img class="avatar avatar--small" src="/images/maria.jpg" alt="" />
<a class="button button--secondary" href="/profile">View profile</a>
</div>
</article>
Mixes: One Element, Two Roles
Sometimes an element is both a standalone block and a part of its parent. BEM calls this a mix. The block provides its own styles, and the parent's element class handles positioning within the parent:
<header class="site-header">
<form class="search-form site-header__search">...</form>
</header>
/* the search-form block knows nothing about headers */
.search-form {
display: flex;
gap: 0.5rem;
}
/* the header decides where the search sits and how wide it is */
.site-header__search {
margin-inline-start: auto;
max-width: 20rem;
}
This keeps components reusable. The search form can go anywhere, and layout concerns stay with the parent.
Elements in Depth
Don't Chain Elements
The most common BEM mistake is mirroring the DOM tree in class names:
/* Avoid */
.card__body__footer__link {
}
An element always belongs to the block, not to another element. Even if the link sits three levels deep, it's still a part of the card:
/* Better */
.card__link {
}
.card__footer {
}
Flat element names mean you can rearrange the markup inside a block without renaming classes.
When an Element Should Become a Block
If an element starts to grow its own elements and modifiers, or you want to reuse it elsewhere, promote it to a block. A .card__author with an avatar, name, bio, and follow button is probably an .author-badge block used inside the card.
Modifiers in Depth
Modifiers represent variations: size, theme, state, or emphasis.
.button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
border: 2px solid transparent;
border-radius: 8px;
font-weight: 600;
background: #0f766e;
color: #ffffff;
}
.button--secondary {
background: transparent;
border-color: currentColor;
color: #0f766e;
}
.button--small {
padding: 0.35rem 0.8rem;
font-size: 0.875rem;
}
.button--full-width {
width: 100%;
justify-content: center;
}
Always Keep the Base Class
A modifier is added alongside the base class, never instead of it:
<!-- Correct -->
<a class="button button--secondary button--small" href="/docs">Docs</a>
<!-- Wrong: loses all the base button styles -->
<a class="button--secondary" href="/docs">Docs</a>
This keeps modifier CSS small, since it only contains what differs.
Key-Value Modifiers
Some teams use a key-value style for modifiers with several options:
.button--size-small
.button--size-large
.alert--type-error
.alert--type-success
It's more verbose, but it groups related options clearly. Pick one style and stick with it.
Modifying Elements from a Block Modifier
When a block modifier needs to change an element, a single level of nesting is acceptable and readable:
.card--featured .card__title {
color: #b45309;
}
It's clearer than adding card__title--featured to the HTML in every place a featured card is rendered.
State Classes
BEM purists write states as modifiers: .nav__link--active, .accordion__panel--open. Many teams instead use short, prefixed state classes, a convention borrowed from SMACSS:
.nav__link.is-active {
font-weight: 700;
border-bottom: 2px solid currentColor;
}
.accordion__panel.is-open {
display: block;
}
Either works. The important thing is consistency. Better still, when there's an ARIA attribute that already expresses the state, style that directly. It keeps visual state and accessible state in sync:
.nav__link[aria-current="page"] {
font-weight: 700;
}
.accordion__trigger[aria-expanded="true"] .accordion__icon {
rotate: 180deg;
}
BEM with Sass and Native Nesting
Sass's parent selector makes BEM compact:
.card {
border-radius: 12px;
&__title {
font-size: 1.25rem;
}
&__link {
color: #0f766e;
&:hover {
text-decoration: underline;
}
}
&--featured {
border: 2px solid #f59e0b;
}
}
This compiles to flat .card__title, .card__link, and .card--featured selectors.
Native CSS nesting, now supported in all current major browsers, doesn't support that concatenation. In native CSS, &__title isn't valid, because & represents a whole selector rather than a string to append to. You can still nest pseudo-classes and modifier combinations:
.card__link {
color: #0f766e;
&:hover {
text-decoration: underline;
}
.card--featured & {
color: #b45309;
}
}
If you rely on &__element syntax, keep using Sass or another preprocessor.
Common Mistakes
- Nesting element names.
.block__elem1__elem2ties your class names to your DOM structure. Keep elements one level deep. - Using elements outside their block.
.card__titleshouldn't appear anywhere except inside a.card. If you need it elsewhere, it's a block. - Styling a modifier without the base class. Modifiers should only contain differences.
- Descriptive-of-appearance names.
.button--bluebreaks when the brand color changes. Use.button--primary. - Adding layout to blocks. Margins that position a block belong to the parent (through a mix or a layout class), otherwise the block can't be reused in a different context.
- Mixing conventions. Half the team writing
card-titleand the other half writingcard__titledefeats the purpose. Add a Stylelint rule likeselector-class-patternto enforce naming.
BEM Alongside Modern CSS
Some ask whether BEM is still needed now that we have CSS Modules, @scope, Shadow DOM, and utility frameworks. Those tools solve the collision problem automatically. BEM still offers readability and a shared vocabulary for components, and it works with any tooling, including plain CSS files in a CMS theme. Many teams also combine approaches: BEM for components, plus a handful of utility classes for spacing and layout.
Conclusion
BEM isn't elegant at first glance, and the double underscores take a week or two to get used to. But the payoff is real: flat specificity, no collisions, markup that explains itself, and components you can refactor or delete with confidence. Keep elements one level deep, always pair modifiers with their base class, name things by purpose rather than appearance, and enforce the convention with a linter.
Try refactoring one messy component in your project with BEM. Once you see how much easier it is to change later, the naming will start to feel natural.


