SLASHED

BEM Naming Convention

The single source of truth for BEM authoring across the SLASHED ecosystem.

Tag legend:


1. Purpose & scope

This document defines the BEM authoring methodology for:

slashed/CONTRIBUTING.md and slashed-blueprints/WIREFRAMES.md link here for the cross-cutting rules. Each repo extends this canon with its own scoping rules — see §2.

BEM-first is the authoring methodology, not a component library. You write everything in BEM. The framework’s .sf-* components are pre-baked common patterns shipped as a convenience; when they don’t fit, author your own block in your own namespace. Either path is fully idiomatic.


2. Namespaces

Every BEM block lives in one of four namespaces. The first three are reserved and lint-enforced; the fourth is consumer-owned with a strong recommendation.

Prefix Where Format Linter Tier
sf-* slashed/css/slashed-*.css .sf-block__element--modifier slashed/bin/lint-bem.py MUST
sb-<filename>NN slashed-blueprints/wireframes/*.html .sb-hero01__grid--featured slashed-blueprints/bin/lint-wireframes.py MUST
sb-pg-<stem> slashed-blueprints/pages/*.html .sb-pg-cart__item--total slashed-blueprints/bin/lint-wireframes.py MUST
<project>-* consumer code .app-card__body none SHOULD

Four shared chrome blocks are also allowed across multiple page templates: sb-pg-header, sb-pg-footer, sb-pg-account, sb-pg-account-nav.

Why prefixes [MUST]

.sf- is reserved for the framework so consumer code never collides with framework selectors and never needs !important to beat them.

.sb- is reserved for everything in slashed-blueprints/. Wireframes add a filename + variant number (sb-hero01, sb-features04) to guarantee zero collisions between wireframe files. Page templates add a pg- sub-prefix and the page filename stem (sb-pg-cart, sb-pg-shop) so copying a template into a host WordPress / WooCommerce theme cannot collide with classes the host theme already ships.

Consumer prefix [SHOULD]

Pick a 2–4 letter project prefix and use it for every block you author. Examples: .app-, .acme-, .shop-. Avoid bare generics like .card, .section, .hero — they will collide eventually.

Why: collisions don’t only come from the framework or blueprints. Host themes (Astra, Bricks, GeneratePress, Avada), page builders (Elementor, Divi, WPBakery), third-party widgets, analytics overlays, and other CSS libraries you might add later all author classes too. A project prefix is a one-time decision that buys you decades of collision immunity.

/* Good — unmistakably yours */
.app-product-card { padding: var(--sf-space-m); }
.acme-hero__cta--primary { /* ... */ }

/* Fragile — will collide */
.card { /* ... */ }
.hero { /* ... */ }

3. Block / element / modifier syntax [MUST]

.block                       /* the block itself */
.block__element              /* an element of the block (`__` separator) */
.block--modifier             /* a flag modifier on the block (`--` separator) */
.block__element--modifier    /* a modifier on an element */

Real examples from the framework:

.sf-card                  /* block */
.sf-card__body            /* element */
.sf-card--bordered        /* block modifier */
.sf-card__title--muted    /* element modifier */

4. kebab-case [MUST]

Multi-word names use - between words. Never camelCase, snake_case, or PascalCase.

.sf-form-field__help-text--error    /* good */
.sf-formField__helpText             /* bad */
.sf_form_field__help_text           /* bad */

5. No element-in-element nesting [MUST]

The DOM nests; class names do not. Flatten.

.sf-card__body__title    /* bad — element-in-element */
.sf-card__title          /* good — flat */

If the inner element is rich enough to need its own elements and modifiers, promote it to its own block:

<article class="sf-card">
  <header class="sf-card-header">
    <h3 class="sf-card-header__title"></h3>
    <p class="sf-card-header__subtitle"></p>
  </header>
</article>

6. Modifiers as binary flags [SHOULD]

Prefer one-word flag modifiers over --key-value shape.

.sf-btn--primary             /* good — flag */
.sf-btn--variant-primary     /* avoid — value-pair shape */

Exception: scale-style modifiers (--xs / --s / --m / --l / --xl) are idiomatic and the linter does not warn on them.


7. Stacking modifiers [MUST]

Apply multiple modifiers as separate classes. Never chain --a--b on one class name.

<!-- Good -->
<button class="sf-btn sf-btn--primary sf-btn--s">Save</button>

<!-- Bad — unparseable: is `primary--s` a value or a stack? -->
<button class="sf-btn--primary--s">Save</button>

8. State classes — native first [MUST / SHOULD / MAY]

For UI state, prefer in this order:

  1. [MUST] Native HTML attribute or pseudo-class. [open], [aria-expanded="true"], :user-invalid, :disabled, :focus-visible, [hidden], [aria-busy="true"]. These work without JS and are accessibility-correct by construction.

  2. [SHOULD] BEM modifier when no native fit exists but the state is bound to a specific block: .sf-form-group--error, .sf-card--featured. Use when the state is set declaratively (server-rendered, or set once and not toggled at runtime).

  3. [MAY] is-* standalone class for JS-only runtime state without a natural BEM home: .is-loading, .is-active. Use sparingly. If the state is block-specific, prefer #2.

<!-- Good — native [open] -->
<details class="sf-disclosure" open></details>

<!-- Good — :user-invalid styles the error state without an extra class -->
<input class="sf-form-group__input" required>

<!-- Good — declarative server-side state -->
<div class="sf-form-group sf-form-group--error"></div>

<!-- OK — JS-toggled state, block-agnostic -->
<button class="sf-btn is-loading" aria-busy="true">Saving…</button>

9. Modifier vs utility — decision rule [MUST]

Semantic variant of a block → modifier. Reusable visual or layout property → utility.

Decision flow:

  1. Does this change describe what the block is (a featured card, a destructive button, a horizontal layout variant)? → modifier.
  2. Is it a one-off visual or spacing tweak (extra padding, centred text)? → utility.
  3. Are you applying the same utility stack to the same block twice? → promote to a modifier.
Need Pattern Why
“Featured” card with bigger title, accent border, larger padding .sf-card--featured modifier Multiple linked changes; semantic concept
Centre-align text in one paragraph .sf-text-center utility Single property, generic intent
One card needs extra padding once .sf-p-l utility One-off spacing tweak
Card with sidebar layout (image left, content right) .sf-card--horizontal modifier Structural variant; semantic concept
Hide a label visually but keep it accessible .sf-sr-only utility Generic technique
Same .sf-p-l .sf-bg-surface-2 .sf-rounded-l stack used on three cards promote to .sf-card--inset modifier Repeated stack → give it a name

10. Instance tokens [SHOULD]

CSS custom properties scoped to a block let consumers override behaviour without writing a selector.

Use:

.sf-card {
  padding: var(--sf-card-padding, var(--sf-space-l));
}
<article class="sf-card" style="--sf-card-padding: 2rem"></article>

Inline style="--token: value" for instance tokens is the framework’s intended escape hatch — preferred over modifier explosion or arbitrary utility stacks.


11. Authoring order [SHOULD]

Match the cascade-layer order:

  1. Blocks — author your own block, or use a shipped .sf-* block.
  2. Layout primitives — wrap blocks in .sf-stack, .sf-cluster, .sf-grid-*, .sf-container.
  3. Utilities — patch the gaps with .sf-text-*, .sf-p-*, .sf-gap-*.
<section class="sf-section">
  <div class="sf-container sf-stack">
    <article class="app-product-card app-product-card--featured">
      <h2 class="app-product-card__title"></h2>
      <p class="app-product-card__body"></p>
    </article>
  </div>
</section>
.app-product-card {
  padding: var(--sf-space-m);
  background: var(--sf-color-surface);
  border-radius: var(--sf-radius-m);
}
.app-product-card--featured { border: 1px solid var(--sf-accent); }
.app-product-card__title { font-size: var(--sf-text-l); }

Style with framework tokens. Consumer blocks inherit the design system by referencing var(--sf-…) tokens, not hardcoded values.

Consumer BEM sits unlayered and wins over every framework layer by default — you never need !important to beat the framework. (The framework’s own print utilities use !important deliberately to override print-time styles; that exception is framework-source-only.)


12. Cheat sheet

/* Block / element / modifier */
.block
.block__element
.block--modifier
.block__element--modifier

/* kebab-case multi-word */
.form-field__help-text--error

/* Multiple modifiers — separate classes, not chained */
class="btn btn--primary btn--s"   /* good */
.btn--primary--s                   /* bad */

/* No element-in-element */
.card__body__title    /* bad */
.card__title          /* good */

/* State — native first */
[open], [aria-expanded="true"], :user-invalid, :disabled, [hidden]
.block--state    /* if no native fit */
.is-loading      /* JS-only fallback */

/* Namespaces */
.sf-*              framework
.sb-<file>NN-*    blueprints wireframes (variant-scoped)
.sb-pg-<stem>-*   blueprints pages
.<project>-*       consumer (recommended: 2–4 letter prefix)

See also