@morev/bem/no-chained-entities SASS-only β
Disallows splitting BEM entities across multiple chained &
selectors in SCSS.
.the-header {
$b: &;
&__primary {
// ...some element styles
&-actions {
// ...some element styles
&-button {
// ...some element styles
&--log-in {
// ...modifier styles
}
&--favorites {
// ...modifier styles
}
}
}
}
}
.the-header {
$b: &;
&__primary {
// ...some element styles
}
&__primary-actions {
// ...some element styles
}
&__primary-actions-button {
// ...some element styles
&--log-in {
// ...modifier styles
}
&--favorites {
// ...modifier styles
}
}
}
Motivation β
At first glance, splitting BEM selectors into smaller nested parts using &
might seem elegant or even efficient β you avoid repetition and reuse existing context.
But in real-world maintenance, this approach quickly turns into a problem.
Imagine you're debugging a button from the example above.
- You see this in the HTML:
class="the-header__primary-actions-button"
; - You recognize it as the
primary-actions-button
element of thethe-header
block; - You open
the-header.scss
and search for it β but nothing comes up.
Why? Because it was built piece by piece:
&__primary {
&-actions {
&-button {}
}
}
Now youβre forced to mentally reconstruct the full class name from nested fragments: &__primary
β &__primary-actions
β &__primary-actions-button
.
This mental effort adds up, especially in large codebases with deep nesting.
But when selectors are written as complete BEM entities at each level, they're easy to find with a simple text search. Looking for an element? You'll find it. Looking for its modifier? It's right there β inside the corresponding element block. No reconstruction, no guesswork.
Long story short β don't split BEM entities βοΈ
Words break by syllables β BEM breaks by entities.
Write each full entity on its own nesting level for clarity and searchability.
Rule options β
All options are optional and have sensible default values.
// π .stylelintrc.js
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-chained-entities': true,
}
}
// π .stylelintrc.js
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-chained-entities': [true, {
disallowNestedModifierValues: false,
separators: {
element: '__',
modifier: '--',
modifierValue: '--',
}
messages: {
element: (actual, expected) => {
return `
β Unexpected chained BEM element ${actual}.
Use ${expected} at parent level instead.
`;
},
// ...other custom messages
},
}],
},
}
Show full type of the rule options
export type NoChainedEntitiesSecondaryOption = {
/**
* Whether to disallow nesting for modifier values.
*
* @default false
*/
disallowNestedModifierValues?: boolean;
/**
* Custom message functions for each violation type.
* If provided, overrides the default error messages.
*/
messages?: {
/**
* Custom message for chained BEM block violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
block?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM element violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
element?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM modifier violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
modifierName?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM modifier value violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
modifierValue?: (actual: string, expected: string) => string;
/**
* Custom message for nested BEM modifier values.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
nestedModifierValue?: (actual: string, expected: string) => string;
};
/**
* Object that defines BEM separators used to distinguish blocks, elements, modifiers, and modifier values. \
* This allows the rule to work correctly with non-standard BEM naming conventions.
*
* @default { element: '__', modifier: '--', modifierValue: '--' }
*/
separators?: {
/**
* Separator between block and element.
*
* @default '__'
*/
element?: string;
/**
* Separator between block/element and modifier name.
*
* @default '--'
*/
modifier?: string;
/**
* Separator between modifier name and modifier value.
*
* @default '--'
*/
modifierValue?: string;
};
};
Show info about Stylelint-wide options
Every rule in this plugin also supports the standard Stylelint per-rule options (disableFix
, severity
, url
, reportDisables
, and message
), even though they are not explicitly reflected in the type definitions to avoid unnecessary noise.
Note: the message
option is technically available, but its use is discouraged: each rule already provides a typed messages
object, which not only offers IDE autocompletion but also supports multiline strings and automatically handles indentation.
For more information, see the official Stylelint configuration docs.
disallowNestedModifierValues
β
Enforces that modifier values must be expressed as flat, single-level selectors rather than nested under modifier names.
/**
* @default false
*/
type DisallowNestedModifierValuesOption = boolean;
Some teams prefer to keep the BEM structure strictly three-leveled, without introducing an extra nesting level for modifier values:
block
[element]
[modifier[-value]]
// β Nested modifier values β not allowed with this option
.app-button {
&--variant {
&--primary {}
&--secondary {}
}
}
// β
Flat modifier values β allowed
.app-button {
&--variant--primary {}
&--variant--secondary {}
}
This keeps selectors simple and consistent, and makes each full modifier easily searchable and traceable in code.
separators
β
The rule supports different naming conventions for BEM entities by allowing you to configure the separators between block elements, modifiers, and modifier values.
This flexibility ensures compatibility with all popular BEM styles described in the official BEM methodology naming convention or even custom ones.
Available separators β
Option | Default | Description |
---|---|---|
element | __ | Separator between block and element. |
modifier | -- | Separator between block/element and modifier name. |
modifierValue | -- | Separator between modifier name and modifier value. |
Rule is separator-agnostic
You can adapt this rule to any BEM naming convention using the available options.
For details on naming principles, refer to the official BEM methodology guide.
messages
β
The rule provides built-in error messages for all violations it detects.
You can customize them using the messages
option. This can be useful to:
- Adjust the tone of voice to match your team's style;
- Translate messages into another language;
- Provide additional project-specific context or documentation links.
INFO
You don't need to override all message functions β or any of them at all.
Each message function receives the detected selector and expected (correct) selector as a suggestion.
Example β
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-chained-entities': [true, {
messages: {
block: (actual, expected) =>
`β Do not split block names like "${actual}". Rewrite to ${expected}.`,
nestedModifierValue: (actual, expected) => {
return `
Do not nest modifier values - prefer flat form.
Write "${expected}" at the parent level instead of "${actual}".
`
},
},
}],
},
}
Show function signatures
export type MessagesOption = {
/**
* Custom message for chained BEM block violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
block?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM element violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
element?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM modifier violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
modifierName?: (actual: string, expected: string) => string;
/**
* Custom message for chained BEM modifier value violations.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
modifierValue?: (actual: string, expected: string) => string;
/**
* Custom message for nested BEM modifier values.
*
* @param actual Actual BEM selector found in the source code.
* @param expected Expected BEM selector.
*
* @returns Error message.
*/
nestedModifierValue?: (actual: string, expected: string) => string;
};
How message formatting works
If your custom message function returns anything other than a string
(e.g., undefined
), the rule will automatically fall back to the default built-in message.
Additionally, all custom messages are automatically processed through stripIndent
function, so it's safe and recommended to use template literals (backticks, `
) for multiline messages without worrying about inconsistent indentation.