Skip to content

@morev/bem/no-side-effects ​

Disallows selectors that apply styles outside the scope of the current BEM block.

scss
.the-component {
  // βœ… OK: pseudo-element on the block
  &::before {}

  // βœ… OK: element within the same block
  &__element {

    // ❌ Side effect: generic element inside BEM element
    div {} 
  }

  // βœ… OK: internal interaction between block elements
  &__link:hover &__foo {}

  // ❌ Side effect: unrelated component selector
  .foo-component {} 
}

// ❌ Side effect: unrelated component selector on the root level
.another-component {} 
🧱 How the BEM block is determined

Some BEM linters β€” like postcss-bem-linter β€” require you to define the block name explicitly using a comment (/** @define the-component */) or derive it from the filename via configuration.

This plugin takes a different, much simpler approach:


The first class selector in the file is considered the BEM block.


βœ… Why this is enough
In a component-oriented architecture, there is rarely a reason to define more than one block per file. Assuming that the first top-level class represents the component's block is usually both practical and predictable β€” without requiring additional annotations or configuration.
It also avoids coupling the rule to naming conventions or file structure.


❓ What if you need more control?
If your project uses a different structure, this assumption may not work well.

This plugin doesn't currently support custom block resolution logic or in-file annotations β€” mostly because there's little evidence it's needed.
But if you have a real use case β€” feel free to open an issue and describe your use case.

Motivation ​

The purpose of this rule is to enforce strict encapsulation within BEM components.

BEM components SHOULD NOT apply styles to other components or elements.
Each component's file is expected to contain styles that are self-contained and local to the block it defines.

When a component styles something outside its own block β€” e.g., targeting other components, elements, or global tags β€” it breaks encapsulation and creates implicit dependencies between unrelated files.

Restricting each file to styling only its own block helps maintain a clear separation of concerns and prevents styles from leaking across component boundaries.

Rule options ​

All options are optional and come with recommended default values.

js
// πŸ“„ .stylelintrc.js

export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': true,
  }
}
js
// πŸ“„ .stylelintrc.js

export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': [true, {
      ignore: ['.swiper-*', 'span'],
      messages: {
        rejected: (selector) =>
          `Do not use side-effects like ${selector}.`,
      }
    }],
  }
}
Show full type of the options
ts
export type NoSideEffectsOptions = {
  /**
   * Selectors to ignore (allowed side-effects). \
   * Each entry can be a string (optionally with wildcards) or a regular expression.
   *
   * @default []
   */
  ignore?: Array<string | RegExp>;

  /**
   * Custom message functions for rule violations.
   * If provided, overrides the default error messages.
   */
  messages?: {
    /**
     * Custom message for a rejected selector.
     *
     * @param   selector   The offending selector, e.g. `> .side-effect`.
     *
     * @returns            The error message to report.
     */
    rejected?: (selector: string) => 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.


ignore ​

Some side effects are either unavoidable or intentionally allowed in certain workflows. The ignore option provides an escape hatch for such cases by letting you explicitly whitelist selectors that should be excluded from validation.

Common use cases ​

  • Third-party integration β€” when targeting DOM structures from libraries like Swiper, Tippy, etc., which don't follow your BEM conventions and cannot be styled otherwise.
  • Minor inline elements β€” in some cases, developers choose not to introduce a dedicated BEM element for simple internal markup (e.g. <span> for currency symbol ), and instead write quick selectors like .block__price span.

While such cases are debatable from a strict BEM perspective, the ignore option gives you control over which selectors to allow based on your team’s standards.

Configuration examples ​

Ignore by exact name
js
export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': [true, {
      ignore: ['.some-component'] 
    },
  },
}

scss
// βœ… Side effect on `.some-component` does not trigger warnings
.the-component .some-component {}

.the-component {
  .some-component {}
}
Ignore by wildcard pattern
js
export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': [true, {
      ignore: ['.swiper-*'] 
    },
  },
}

scss
// βœ… Side effects on classes starting with `.swiper-` do not trigger warnings.
.the-component {
  .swiper-slide {}
  .swiper-pagination {}
  .swiper-navigation {}
}
Ignore by regular expression
js
export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': [true, {
      ignore: [/.*swiper.*/] 
    },
  },
}

scss
// βœ… Any side effects containing `swiper` do not trigger warnings.
.the-component {
  div span .swiper-slide a {}
}

Notes ​

  • Each resolved side-effect selector is tested against all patterns in the list.
    If at least one pattern matches, the selector is skipped and no warning is reported.
  • Matching is performed against the entire resolved side-effect selector, not its individual parts.
    For example, if the selector .block__price span is reported, then 'span' does not match β€” use '*span' (string) or /.*\sspan/ (RegExp) to allow such side-effects.
  • You can mix plain strings, wildcards, and regular expressions in the same list.

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.

Message function receives the detected side-effect selector as an argument.

Example ​

js
export default {
  plugins: ['@morev/stylelint-plugin'],
  rules: {
    '@morev/bem/no-side-effects': [true, {
      messages: {
        rejected: (selector) =>
          `β›” Do not use side-effects like "${selector}".`,
        },
      },
    }],
  },
}
Show function signature
ts
export type MessagesOption = {
  /**
   * Custom message for a rejected selector.
   *
   * @param   selector   The offending selector, e.g. `> .side-effect`.
   *
   * @returns            The error message to report.
   */
  rejected?: (selector: 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.