The single source of truth for BEM authoring across the SLASHED ecosystem.
Tag legend:
This document defines the BEM authoring methodology for:
codeslash-dev/slashed),codeslash-dev/slashed-blueprints),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.
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.
.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.
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 { /* ... */ }
.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 */
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 */
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>
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.
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>
For UI state, prefer in this order:
[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.
[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).
[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>
Semantic variant of a block → modifier. Reusable visual or layout property → utility.
Decision flow:
| 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 |
CSS custom properties scoped to a block let consumers override behaviour without writing a selector.
--sf-<block>-<prop> — e.g. --sf-grid-cols,
--sf-btn-bg, --sf-card-padding, --sf-spin-duration.--<block>-<prop> — e.g. --card-padding,
--app-hero-bg.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.
Match the cascade-layer order:
.sf-* block..sf-stack, .sf-cluster,
.sf-grid-*, .sf-container..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.)
/* 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)
slashed/CONTRIBUTING.md — repo layout, gotchas,
versioning, build process, testing.slashed-blueprints/WIREFRAMES.md
— blueprint-specific extensions: forbidden visual utilities,
mobile-first wireframes, accessibility checklist, .sb-<file>NN and
.sb-pg-<stem> namespaces.slashed/bin/lint-bem.py — enforces the MUST rules on framework CSS.slashed-blueprints/bin/lint-wireframes.py — enforces the MUST rules
on blueprint HTML, plus blueprint-local discipline (forbidden visual
utilities, mobile-first, namespace prefixes).