Методологию БЭМ придумали в Яндексе больше десяти лет назад. За это время фронтенд сильно изменился: появились компоненты, фреймворки, утилитарные классы и прочее. Казалось бы, БЭМ должен был уйти в архив. Но нет - он до сих пор спасает проекты от CSS-хаоса. Особенно когда речь заходит о Vue.

Почему? Потому что Vue думает компонентами, а БЭМ думает независимыми блоками. Они не конфликтуют. Они дополняют друг друга. Если у вас уже есть понятная, проверенная система - не стоит изобретать велосипед ради «современного» подхода. Архитектура важнее моды.

Прежде чем переходить к коду, давайте быстро вспомним, что такое БЭМ. Без занудства.

Три кита БЭМ

  • Блок - самостоятельный компонент. Он не должен зависеть от контекста: никаких margin снаружи, никаких привязок к родительским стилям. Пример: .header, .card, .button.

  • Элемент - часть блока, которая не существует отдельно. Имя пишется через двойное подчёркивание: .button__icon, .card__title.

  • Модификатор - состояние или вариант блока/элемента. Пишется через одно подчёркивание: .button_disabled, .button_size_l, .card_theme_dark.

Как это работает во Vue

Во Vue стили обычно пишут прямо в компоненте. И тут начинается главный вопрос: scoped или нет? :deep() или нет? Один <style> или несколько?

Правило простое, но его часто нарушают:

  • Всегда используйте scoped, если стили относятся только к этому компоненту. Это изолирует зону ответственности.

  • :deep() - не зло, но это «аварийный выход». Применяйте его только когда работаете с сторонними UI-библиотеками или когда без него действительно никак. Не используйте его, чтобы обойти продуманную структуру.

Архитектура ломается не из-за технологий, а из-за решений «сейчас по-быстрому». БЭМ как раз заставляет думать на шаг вперёд.

Разбираем на практике

Допустим, нам нужна кнопка. Она может быть основной, ссылкой, аватаром. Включаться/выключаться. Менять размер и цвет. Без системы это превращается в кашу:

<template>
  <button :class="{
  'button-basic': isBasic,
  'button-href': isHref,
  'button-avatar': isAvatar 
 }">
    {{ text }}
  </button>
</template>

Что не так?

  1. Классы не связаны логически. button-avatar - это вообще состояние, тип или вариант размещения?

  2. При добавлении новых состояний (loading, disabled, size-xl) маппинг разрастается.

  3. Через полгода никто не вспомнит, какой класс за что отвечает. А править стили будет страшно.

Теперь тот же компонент, но с БЭМ:

<template>
  <button :class="[
    'button',
    {
      'button_disabled': disabled,
      'button_loading': loading,
    },
    {
      'button_size_xl': size === 'xl',
      'button_size_l': size === 'l',
    },
    {
      'button_primary': color === 'primary',
      'button_secondary': color === 'secondary',
    }
  ]">
    {{ text }}
  </button>
</template>

Разница в голове. Вы сразу видите:

  • Это блок .button.

  • У него есть состояния (_disabled, _loading).

  • Есть варианты размера и цвета.

  • Ничего не переплетается. Каждый класс - одна задача.

А CSS при этом становится предсказуемым:

.button {
  /* базовые стили */
  ...
}

.button_disabled {
  opacity: 0.5;
  ...
}

.button_loading {
  /* стили состояния загрузки */
}

.button_primary:not(.button_disabled, .button_loading):hover {
  background-color: #0055ff;
}

Обратите внимание на :not(). Это не требование БЭМ, а приятный бонус. Вы не плодите лишние классы вроде .button_primary_not_disabled. CSS сам фильтрует состояния. Чисто, масштабируемо, легко читать.

Когда БЭМ не нужен?

Если вы пишете лендинг на 2 экрана или прототип на выходные - не мучайте себя. Tailwind или обычные классы справятся быстрее. БЭМ раскрывается там, где есть команда, долгая поддержка и сложная UI-система.

Какой же итог?

Vue отвечает за логику и реактивность. БЭМ отвечает за предсказуемость стилей. Вместе они дают компонент, который не ломается при масштабировании, не пугает новых разработчиков и не превращается в технический долг через полгода.

Не нужно фанатизма. Не нужно слепого следования правилам. Нужно понимание, зачем вы пишете каждый класс. БЭМ просто напоминает об этом.

Попробуйте в следующем компоненте. Если через неделю не станет легче - вернитесь к тому, как было. Но скорее всего, вы уже не захотите.

Комментарии (6)


  1. vanxant
    11.04.2026 10:02

    Ну те. из статьи следует, что кнопку-аватар с БЭМ вообще сделать нельзя:)


    1. Pnym Автор
      11.04.2026 10:02

      Можно, просто проще было привести пример с button. Если вам необходимо реализовать специальную кнопку для аватара, то тут вообще можно несколько вариантов предложить:

      • на основе button.vue сделать avatar-button.vue;

      • понять, что значит avatar-button. Ну, например, это передача Icon и border-radius: 30px, тогда проще как будто отдельные классы сделать


  1. irtek
    11.04.2026 10:02

    :deep() в Vue это как !important в css аварийные выходы, которые лучше не использовать )


  1. MLG_Noscope
    11.04.2026 10:02

    А зачем мне эта портянка БЭМа в компоненте, когда я могу динамически ставить класс типа :class=styles[size-${props.size}


    1. Pnym Автор
      11.04.2026 10:02

      Спасибо за вопрос, это действительно рабочий и частый паттерн. Но у него есть обратная сторона, которая всплывает не на старте, а когда проект живёт дольше полугода или над ним работает больше одного человека.

      Главная проблема подхода :class="styles['size-' + size]"- он рвёт статическую связь между шаблоном и <style>. Для человека это читаемо, но для всей экосистемы разработки эти классы становятся «невидимыми» до момента выполнения кода.

      Что конкретно ломается:
      - Линтинг и поиск неиспользуемых классовstylelint, purgecss, eslint-plugin-vue и подобные инструменты работают на статическом анализе. Когда класс собирается динамически, линтер не видит его в шаблоне.

      - Явность vs Магияbutton_size_l сразу читается как «модификатор размера у блока button». styles['size-' + size] требует лезть в JS-объект, смотреть маппинг и гадать, какие ключи вообще существуют. В команде из 3+ разработчиков это превращается в квест.

      Динамические маппинги имеют полное право на жизнь: когда вы работаете с CSS-переменными, утилитарными фреймворками или когда стили действительно генерируются на лету. Но для компонентной UI-системы, где важна долгосрочная поддержка, явный :class с БЭМ-модификаторами выигрывает за счёт прозрачности для разрабов и инструментов.


  1. 900k
    11.04.2026 10:02

    import styles from "./exercise.component.module.css";
    
    ...
    ...
    ...
    ...
    
    getHTML() {
        return `
          <div class="page-head" style="display:none;">
            <a href="/books/${this.bookId}" onclick="{{root.goBack(event)}}" class="${styles.back}">Назад к упражнениям</a>
            <h1 class="page-title">Упражнение</h1>
          </div>
          
          <div let-exercise="{{root.appService.exercise$::rx}}" attached="{{exercise}}" style="display:none;">
            <div class="${styles.titleArabic}">{{exercise?.title_arabic || ''}}</div>
            <div class="${styles.titleRussian} mt_s">{{exercise?.title_russian || ''}}</div>
            <div class="${styles.typeText} mt_xs">{{exercise?.type_icon_text || exercise?.type || ''}}</div>
          </div>`
    }

    Самый топ сейчас – это css modules. Я давно в разработке и мне нравился БЭМ, но недавно познакомился с css modules и сразу внедрил их в стартер своего фреймворка