@morev/bem/no-misplaced-relational-styles
Requires relational styles for a BEM entity to be declared within that entity.
.block {
$b: #{&};
&__link {
// ❌ Styles for `label` are owned by `link`.
&:hover #{$b}__label {}
}
&__icon {
// ✅ The target `icon` owns its reaction to `button`.
#{$b}__button:hover & {}
}
}Motivation
Contextual selectors often express that the state of one entity changes another entity. It may feel natural to declare such a relation next to its source — for example, to place label styles inside a link because :hover on the link triggers them.
.block__label {
color: black;
}
.block__link {
&:hover .block__label {
color: red;
}
}However, this scatters styles targeting the label across declarations owned by other entities.
A BEM entity should be self-contained: once a developer finds its declaration, they should be able to trust that all of its states and contextual reactions are located nearby, inside that entity.
.block__label {
color: black;
.block__link:hover & {
color: red;
}
}Reading, changing, or removing it then does not require searching through sibling entities for additional selectors that happen to target it.
This follows the same locality principle as @morev/base/no-selectors-in-at-rules: that rule keeps conditional declarations inside the selector they modify, while this rule keeps cross-entity reactions inside the BEM entity they modify. In both cases, finding the entity once gives you one predictable place to understand and edit its behavior.
// All states and contextual reactions are colocated in one declaration.
// Find the entity once to see its complete behavior in every supported context.
.block__label {
color: black;
// Relational state: another BEM entity changes this entity.
.block__link:hover & {
color: red;
}
// Local state: the entity reacts to its own interaction.
&:hover {
color: blue;
}
// DOM state: application state exposed through an attribute.
&[aria-current='true'] {
font-weight: 700;
}
// Environmental context: the viewport changes the entity's presentation.
@media (width >= 768px) {
font-size: 1.25rem;
}
// Capability context: the browser supports an enhanced presentation.
@supports (text-wrap: balance) {
text-wrap: balance;
}
// Ancestor context: the surrounding theme affects the entity.
[data-theme='dark'] & {
color: white;
}
}Explicit ownership also makes refactoring safer: removing an entity removes its contextual styles with it instead of leaving stale selectors elsewhere in the component.
The rule complements @morev/bem/no-side-effects:
no-side-effectsprevents a component file from styling content outside its BEM block;no-misplaced-relational-stylesverifies ownership of relations inside the block.
Modifiers
Modifiers are independent owners:
.block {
&__item {
// Owns styles for `.block__item`.
&--active {
// Owns styles for `.block__item--active`.
}
}
}In SCSS, a modifier can be nested under its base entity for declaration convenience, but it still creates a separate ownership scope. Contextual styles targeting the modifier therefore belong inside the modifier:
.block {
&__item {
.block:hover &--active {}
}
}.block {
&__item {
&--active {
.block:hover & {}
}
}
}CSS mechanics
With native CSS Nesting, the surrounding BEM entity owns the styles applied by a nested relation. Native nesting does not concatenate identifiers. An element or modifier therefore uses its full BEM selector as the outer owner:
.block__item--active {
.block:hover & {}
}Flat relational selectors have no owner and are rejected:
.block__link:hover .block__label {} /* ❌ */Rule options
All options are optional and come with recommended default values.
// 📄 .stylelintrc.js
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-misplaced-relational-styles': true,
}
}// 📄 .stylelintrc.js
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-misplaced-relational-styles': [true, {
separators: {
element: '__',
modifier: '--',
modifierValue: '--',
},
messages: {
misplaced: (target, owner) =>
`Move ${target} from ${owner ?? 'root'} into its own styles.`,
},
}],
},
}Show full type of the options
export type NoMisplacedSideEffectsOptions = {
/**
* 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.
*/
separators?: {
/**
* String used as the BEM element separator.
*
* @default '__'
*/
element?: string;
/**
* String used as the BEM modifier separator.
*
* @default '--'
*/
modifier?: string;
/**
* String used as the BEM modifier value separator.
*
* @default '--'
*/
modifierValue?: string;
}
/**
* Custom message functions for rule violations.
* If provided, overrides the default error messages.
*/
messages?: {
/**
* Custom message for relational styles declared outside their target BEM entity.
*
* @param target Target BEM entity.
* @param owner BEM entity that currently owns the styles, or `undefined` for a detached selector.
*
* @returns The error message to report.
*/
misplaced?: (target: string, owner: string | undefined) => 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, <!-- eslint-disable-line -- Global ID --> which not only offers IDE autocompletion but also supports multiline strings and automatically handles indentation.
For more information, see the official Stylelint configuration docs.
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][bem-guide] 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. |
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.
The message function receives the target BEM entity and the entity that currently owns its styles.
For detached top-level selectors, owner is undefined.
Example
export default {
plugins: ['@morev/stylelint-plugin'],
rules: {
'@morev/bem/no-misplaced-relational-styles': [true, {
messages: {
misplaced: (target, owner) => owner
? `⛔ Move "${target}" from "${owner}" into its own styles.`
: `⛔ Move "${target}" into its own styles.`,
},
}],
},
}Show function signature
export type MessagesOption = {
/**
* Custom message for relational styles declared outside their target BEM entity.
*
* @param target Target BEM entity.
* @param owner BEM entity that currently owns the styles, or `undefined` for a detached selector.
*
* @returns The error message to report.
*/
misplaced?: (target: string, owner: string | undefined) => 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.