Skip to content

@morev/bem/no-chained-entities SASS-only ​

Disallows splitting BEM entities across multiple chained & selectors in SCSS.

scss
.the-header {
  $b: &;

  &__primary {
    // ...some element styles

    &-actions {
      // ...some element styles

      &-button {
        // ...some element styles

        &--log-in {
          // ...modifier styles
        }

        &--favorites {
          // ...modifier styles
        }
      }
    }
  }
}
scss
.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 the the-header block;
  • You open the-header.scss and search for it β€” but nothing comes up.

Why? Because it was built piece by piece:

scss
&__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.

js
// πŸ“„ .stylelintrc.js

export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-chained-entities': true,
  }
}
js
// πŸ“„ .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
ts
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.

ts
/**
 * @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:

  1. block
  2. [element]
  3. [modifier[-value]]
scss
// ❌ 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 ​

OptionDefaultDescription
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 ​

js
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
ts
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.

Released under the MIT License.