Sass + BEM
BEM (Block, Element, Modifier) is a naming convention that keeps CSS predictable at scale: .block__element--modifier. Sass’s nesting plus & selector composition makes BEM ergonomic to write without exploding selectors.
Block, element, modifier, nesting, &
EXAMPLE
// 1) The shape
// .block — independent component
// .block__element — child piece of a block (double underscore)
// .block--modifier — variant or state of the block (double dash)
// .block__element--modifier — modifier on an element
// 2) Plain CSS — verbose
.card { /* … */ }
.card__title { /* … */ }
.card__body { /* … */ }
.card--featured { /* … */ }
.card--featured .card__title { /* … */ }
// 3) Sass — same idea with & nesting
.card {
padding: 1rem;
border-radius: 0.5rem;
background: var(--surface);
&__title {
font-size: 1.25rem;
font-weight: 600;
}
&__body {
margin-top: 0.5rem;
color: var(--text-muted);
}
&__action {
margin-top: 1rem;
display: inline-block;
}
&--featured {
background: var(--accent);
color: var(--accent-fg);
& .card__title {
color: inherit;
}
}
&--compact {
padding: 0.5rem;
.card__body { font-size: 0.875rem; }
}
}
// 4) Resulting CSS
// .card { … }
// .card__title { … }
// .card__body { … }
// .card--featured { … }
// .card--featured .card__title { … }
// .card--compact { … }
// .card--compact .card__body { … }
// 5) Modifier patterns
// • Boolean — .card--featured (style depends on the modifier)
// • Key/value — .card--variant-success, .card--size-lg
// • State — .card--is-open, .card--is-loading
.button {
padding: 0.5rem 1rem;
&--variant-primary {
background: var(--accent);
color: var(--accent-fg);
}
&--variant-secondary {
background: transparent;
border: 1px solid var(--border);
}
&--size-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
&--size-lg { padding: 0.75rem 1.25rem; font-size: 1.125rem; }
}
// 6) Don't nest elements inside other elements
// .card__body__title is WRONG.
// Either it's another element of .card (.card__subtitle) or you've found a sub-component.
// 7) State classes — separate from BEM, lowercase prefix
.is-open { display: block; }
.is-hidden { display: none; }
.is-loading { opacity: 0.5; pointer-events: none; }
.card {
&.is-loading { /* … */ }
.card__title { /* … */ }
}
// State classes describe transient state (added/removed by JS); modifiers describe variants.
// 8) Mixins for repeated modifier groups
@mixin button-variant($bg, $fg, $hover-bg) {
background: $bg;
color: $fg;
&:hover, &:focus-visible { background: $hover-bg; }
}
.button {
&--primary { @include button-variant(#4f46e5, #fff, #4338ca); }
&--success { @include button-variant(#16a34a, #fff, #15803d); }
&--danger { @include button-variant(#dc2626, #fff, #b91c1c); }
}
// 9) BEM + responsive
.card {
padding: 1rem;
@media (min-width: 768px) {
padding: 1.5rem;
&__title { font-size: 1.5rem; }
}
}
// 10) BEM + utility classes — pragmatic mix
// BEM for COMPONENTS (.card, .nav, .form)
// Utilities for ONE-OFFS (.mt-4, .text-center)
// Avoid utility soup AND avoid hyper-componenting everything.
<div class="card card--featured mt-4">
<h3 class="card__title text-center">Title</h3>
<p class="card__body">Body</p>
</div>
// 11) Avoiding the deep-nesting trap
// Sass nesting > 3 levels is a smell — output selectors get long, specificity explodes.
// BEM helps because each piece is one class, so & only nests one level for elements.
// 12) File organisation
// scss/
// components/
// _card.scss — .card block
// _button.scss — .button block
// _navbar.scss — .navbar block
// utilities/
// _spacing.scss
// abstracts/
// _tokens.scss
// _mixins.scss
// main.scss
// @use 'abstracts/tokens';
// @use 'components/card';
// @use 'components/button';
// ...
// 13) BEM vs other methodologies
// BEM — explicit naming, predictable
// OOCSS — composition by single-purpose classes
// SMACSS — categorisation (base, layout, module, state)
// Utility-first — Tailwind: skip naming entirely
// CSS Modules — automatic scoping; no naming needed
// CSS-in-JS — JS-scoped styles, dynamic
// Pick one, document the convention, enforce in code review.
// 14) Common bugs
// • Long selectors (.foo .bar .baz) from over-nesting → BEM keeps it flat
// • Block-on-block coupling (.card-list .card__title) → bad coupling; use mix-ins instead
// • Re-defining BEM separators per file — settle on one (.block__element--modifier)
// • Confusing element with sub-block — if it has its own meaningful state, it's a new block
// • Using BEM AND Tailwind in the same project without rules — pick one for components
// • Underscore typos (.card_title) — linter (stylelint-bem-pattern) catches these
Why it matters
BEM keeps CSS predictable: one class per piece, no specificity wars, no surprise overrides. Sass nesting with & makes the convention pleasant to write, but resist nesting more than three levels deep — BEM’s point is FLAT selectors that don’t depend on DOM structure.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
.block {
&__element { color: red; }
&--modifier { font-weight: bold; }
}
// Outputs: .block__element, .block--modifier
Try it Yourself »
Discussion
Loading…