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
!importantwars - Self-documenting —
.search-form__input--disabledneeds no comment - Encapsulation —
.card__titlewon’t bleed into.header__title - Grep-friendly — search
.cardto 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