BEM: Blocks, Elements, and Modifiers

BEM (Block, Element, Modifier) is a CSS naming convention created by Yandex in 2009. The pattern is .block__element--modifier.

  • Block — a standalone component (.card)
  • Element — a part of that block that has no meaning on its own (.card__title)
  • Modifier — a variation or state of a block/element (.card--featured, .card__title--large)
.card { }              /* Block */
.card__title { }       /* Element */
.card--featured { }    /* Modifier */

The double underscore (__) signals “this belongs to,” and the double hyphen (--) signals “this is a variant of.” Elements are always flat — .card__body__text is wrong, .card__text is right.

Why BEM?

  • Flat specificity — single-class selectors only, no !important wars
  • Self-documenting.search-form__input--disabled needs no comment
  • Encapsulation.card__title won’t bleed into .header__title
  • Grep-friendly — search .card to find everything related to that block

DX tradeoffs

Good: readable diffs, easy onboarding, human-readable DevTools classes (.search-form__input--error vs .sc-dkPtRN).

Bad: verbose class names, naming fatigue (“is this a block or an element?”), awkward JS access — $style['card__body--muted'] because -- breaks dot notation.

Use Case in Vue

In Vue SFCs, you probably don’t need BEM. Vue’s <style scoped> and <style module> already handle encapsulation, so BEM’s namespacing becomes redundant. Simpler alternatives:

Flat semantic names with scoped styles — just .title, .body, .muted. The scoping attribute ([data-v-f3f3eg9]) prevents collisions.

CSS Modules with camelCase — better when you need JS class binding:

<template>
  <div :class="$style.card">
    <h2 :class="$style.title">Hello</h2>
    <p :class="[$style.body, $style.bodyMuted]">World</p>
  </div>
</template>

<style module>
.card { /* ... */ }
.title { /* ... */ }
.body { /* ... */ }
.bodyMuted { /* ... */ }
</style>

Clean dot notation, no brackets. Configure with css.modules.localsConvention: 'camelCaseOnly' in Vite.

Data attributes for states:data-featured="featured" instead of modifier classes. No class binding juggling, and state shows up clearly in DevTools.

When to use what

  • BEM — shared/global CSS, design systems, vanilla CSS projects, large teams
  • Scoped styles + flat names — Vue/Astro components that don’t need JS class binding
  • CSS Modules + camelCase — components that need dynamic class binding in JS
  • Utility-first (Tailwind) — when you’d rather skip naming altogether