javascript

БЭМ + Tailwind: прагматичный гибрид для современного фронтенда

  • суббота, 5 июля 2025 г. в 00:00:05
https://habr.com/ru/articles/924826/

Введение: конец войны методологий

АААААААААААА!!! 15 МИНУТ МОЕЙ ЖИЗНИ ТОЛЬКО ЧТО УШЛИ НА ПРИДУМЫВАНИЕ ИМЕНИ ГРЁБАНОМУ DIV-КОНТЕЙНЕРУ!

Знакомо, да? Не я один, кто неистово бился головой о клавиатуру, пытаясь придумать, как назвать очередную обертку для обертки внутри обертки?

Я УСТАЛ. Мы все устали. Устали от:

  • Бесконечного .header__nav-container__menu-wrapper__item-list__element 😵‍💫

  • Спора на код-ревью о том, должен ли это быть block__element или просто новый block 🤬

  • "Не используй margin на блоке! Это нарушает принципы БЭМ!" — и последующего создания еще одного контейнера 🤦‍♂️

  • Конфликтов имён классов в разных компонентах ⚔️

  • А потом приходит Tailwind и превращает твой HTML в нечитаемую простыню из 20 классов на элемент 📜

"Я просто хотел сверстать карточку товара, а закончил философским вопросом о природе существования блоков и элементов."

В какой-то момент я понял, что трачу больше времени на придумывание названий классов, чем на написание самого CSS. И подозреваю, что не я один такой.

Но что, если я скажу, что можно прекратить эти мучения?

В этой статье я расскажу, как я наконец-то перестал биться в конвульсиях при создании очередного div'а и нашел баланс между структурированностью БЭМа и удобством утилитарного подхода Tailwind. Больше никаких религиозных войн и догматизма — только прагматизм и эффективность.

Проблемы чистого БЭМа в реальном мире

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

<!-- Ад нейминга -->
<div class="content-block">
  <div class="content-block__wrapper">
    <div class="content-block__wrapper-inner">
      <div class="content-block__wrapper-inner-container">
        <!-- Наконец-то контент! Осталось только пробраться через эти девять кругов ада нейминга -->
      </div>
    </div>
  </div>
</div>
  1. Мучительный нейминг для вспомогательных элементов и контейнеров (Серьезно, кто придумал, что все должно иметь осмысленное имя? Моя кошка и та не страдает, когда я называю её "кис-кис-кис")

  2. Проблема с отступами — в чистом БЭМе блок не должен иметь margin, поэтому приходится создавать дополнительные обертки (Потому что, видите ли, margin — это как НЛО: все знают, что они существуют, но никто не признается в их использовании)

  3. Раздутая разметка из-за множества уровней вложенности (Ничто так не поднимает самооценку, как ощущение, что ты архитектор, спроектировавший Бурдж-Халифу из div'ов)

  4. Сложность переиспользования для позиционирования — приходится создавать множество модификаторов (block_position-top_margin-left_slightly-to-the-right-no-not-that-right-a-bit-more... идеально!)

С другой стороны, чистый Tailwind часто приводит к "салянке" классов, потере семантики и усложнению чтения HTML. (Потому что ничто не говорит "я знаю, что делаю" лучше, чем 42 класса на одном элементе)

Гибридный подход: когда что использовать

1. Используй БЭМ с SCSS и @apply, когда:

  • Имеешь дело с семантически очевидными компонентами: .product-card, .user-profile, .catalog (Ключевое слово: "очевидными". Если ты полчаса думаешь, как назвать div, это уже не очевидно, Капитан Очевидность!)

  • Работаешь с компонентами с богатой внутренней структурой (Как в большой семье — каждый элемент должен знать своё место и родословную до пятого колена)

  • Создаешь переиспользуемые компоненты с разными вариациями (Потому что DRY расшифровывается как "Don't Repeat Yourself", а не "Damn, Repeat Yelp!")

// SCSS с использованием @apply для утилит Tailwind
.product-card {
  @apply bg-white rounded-lg shadow-sm overflow-hidden;
  // 👆 Смотри-ка! Одна строка вместо 10 строк CSS! Какая экономия, прямо как в рекламе стирального порошка!

  &__image {
    @apply w-full object-cover aspect-video;
  }

  &__body {
    @apply p-4;
  }

  &__title {
    @apply text-lg font-medium text-gray-900;
  }

  &__price {
    @apply text-primary-600 font-bold mt-2;
    // Ой, margin-top! Не показывайте это БЭМ-пуристам 🙈

    &_discounted {
      @apply line-through text-gray-400 font-normal;
    }
  }

  // Модификатор для самого блока
  &_featured {
    @apply shadow-md border-2 border-primary-500;
    // А здесь можно было бы создать еще один блок "featured-card",
    // пять оберток и два миксина. Но мы не ищем легких путей, верно?
  }
}

Что дает:

  • Четкую структуру компонента (Как государство, но без налогов)

  • Легкое переиспользование (Когда-нибудь ваш компонент вырастет и станет библиотекой, и вы будете им гордиться!)

  • Хорошую читаемость благодаря SCSS (Потому что нормальные люди не хотят читать CSS, где селекторы длиннее, чем названия лекарств)

  • Повторно используемые стили утилит благодаря @apply (Tailwind как пицца — берешь только те кусочки, которые нравятся)

2. Используй чистый Tailwind, когда:

  • Работаешь с вспомогательными контейнерами и обертками (Те самые, которым мы всегда даем имена типа wrapper, container, box, и — в момент отчаяния — thingy)

  • Создаешь однократные элементы без особой семантики (Потому что не всё заслуживает имени, некоторым достаточно просто быть flex-1 p-4)

  • Имеешь сложности с придумыванием осмысленного имени (Когда все хорошие имена уже заняты, а плохие не пропустит код-ревью)

<!-- Вместо мучений с .layout__content-wrapper__inner-container -->
<div class="max-w-screen-xl mx-auto px-4 py-8">
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    <!-- Содержимое, которое не нужно искать в CSS-файлах за тридевять земель -->
  </div>
</div>

Что дает:

  • Меньше головной боли с названиями (Потому что "придумывание имен" должно относиться к детям и домашним питомцам, а не к div'ам)

  • Быстрая разработка (Ты вернешься домой к ужину, а не будешь до полуночи подбирать имя для очередного элемента)

  • Отсутствие дополнительных CSS-файлов для вспомогательных элементов (Меньше файлов = меньше мест, где можно потеряться)

Решение проблемы с margin и позиционированием

Вот где гибридный подход по-настоящему сияет. ✨ Вместо создания модификаторов для каждого варианта отступа и позиционирования (я тебя умоляю, кому нужны .product-card_margin-bottom-16 .product-card_margin-bottom-32 .product-card_margin-left-0-on-mobile?!), используем Tailwind поверх БЭМ-классов:

<!-- Внутренняя структура через БЭМ, позиционирование через Tailwind -->
<article class="product-card mb-4 md:mb-0 md:mr-6 flex-1">
  <img
    class="product-card__image"
    src="product.jpg"
    alt="Product"
  />
  <div class="product-card__body">
    <h3 class="product-card__title">Отличный продукт</h3>
    <div class="flex items-center justify-between mt-3">
      <!-- Да, я использовал флексы внутри БЭМ-блока! Арестуйте меня! -->
      <p class="product-card__price">1999 ₽</p>
      <button class="product-card__button">Купить</button>
    </div>
  </div>
</article>

Обратите внимание:

  • product-card и его внутренние элементы следуют БЭМ (Потому что мы воспитанные люди)

  • Внешние отступы (mb-4, md:mb-0, md:mr-6) и некоторое позиционирование (flex-1) задаются через Tailwind (Потому что жизнь слишком коротка, чтобы создавать модификаторы для каждого пикселя отступа)

  • Для уникальной внутренней структуры, которую сложно выразить в БЭМ, также используем Tailwind (flex items-center justify-between mt-3) (Потому что иногда flex — это просто flex, а не product-card__price-button-row_layout-horizontal)

Переиспользуемые секции: база через БЭМ, вариации через Tailwind

Особенно полезен гибридный подход при работе с секциями сайта, которые переиспользуются в разных контекстах (Ох уж эти секции, которые выглядят одинаково, но дизайнер говорит "здесь отступ должен быть чуть побольше, а здесь — градиент поярче"):

<!-- Базовая структура через БЭМ, специфичные стили через Tailwind -->
<section
  class="hero-section bg-gradient-to-r from-blue-500 to-indigo-600 py-16 md:py-24"
>
  <div class="container mx-auto px-4">
    <h1 class="hero-section__title text-center md:text-left"
      >Заголовок страницы</h1
    >
    <p class="hero-section__subtitle mb-8 max-w-2xl"
      >Подробное описание сервиса</p
    >
    <div class="hero-section__buttons flex flex-col sm:flex-row gap-4">
      <!-- Да, мы используем gap вместо margin между кнопками!
           И нет, нам не стыдно! Это 2023-й, а не каменный век! -->
      <button class="hero-section__button_primary">Начать</button>
      <button class="hero-section__button_secondary">Подробнее</button>
    </div>
  </div>
</section>
.hero-section {
  &__title {
    @apply text-3xl md:text-5xl font-bold text-white;
    // Мама, смотри! Я адаптивную типографику без медиа-запросов сделал!
  }

  &__subtitle {
    @apply text-lg text-white/80;
  }

  &__button {
    &_primary {
      @apply bg-white text-blue-600 font-medium py-3 px-6 rounded-lg;
    }

    &_secondary {
      @apply bg-transparent border-2 border-white text-white py-3 px-6 rounded-lg;
      // 15 строк CSS в одну! Дизайнер даже не заметит подмены!
    }
  }
}

Такой подход позволяет переиспользовать секции, настраивая их внешний вид и позиционирование через Tailwind прямо в месте использования. (А не через 50 модификаторов, имена которых заставляют задуматься о смене профессии)

Работа с компонентами Vue/React/Angular

В мире компонентов гибридный подход тоже прекрасно работает (если ваш тимлид не фанатик какой-то одной методологии):

<!-- ProductCard.vue -->
<template>
  <article
    class="product-card"
  >
    <img
      class="product-card__image"
      :src="product.image"
      :alt="product.name"
    />
    <div class="product-card__body">
      <h3 class="product-card__title">{{ product.name }}</h3>
      <p
        class="product-card__price"
        :class="{ 'product-card__price_discounted': product.hasDiscount }"
      >
        {{ product.price }}
      </p>
    </div>
  </article>
</template>

И используем его так:

<template>
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    <ProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      class="mb-4 md:mb-0"
      <!-- Смотрите, никаких custom-component-margin-for-this-specific-case модификаторов! -->
    />
  </div>
</template>

Когда НЕ стоит использовать гибридный подход

  1. Микро-проекты — для совсем небольших проектов часто достаточно просто Tailwind (Чтобы потом через 6 месяцев проклинать себя, когда "маленький проект" вырастет в монстра, но кто ж об этом думает в начале?)

  2. Строгие требования к методологии — если в команде есть строгие гайдлайны по БЭМ или Tailwind (Или если ваш тимлид носит футболку "БЭМ или смерть")

  3. Проекты с устаревшим CSS — может быть сложно интегрировать в унаследованную кодовую базу (Особенно если там есть !important в каждой второй строке и inline-стили "потому что так быстрее")

Заключение

Гибридный подход БЭМ + Tailwind это не ересь (хотя пуристы с обеих сторон могут сжечь вас на костре), а прагматичное решение реальных проблем:

  • Перестаем мучиться с неймингом вспомогательных элементов (Нет больше "как назвать этот div, который просто центрирует контент?")

  • Решаем проблему с отступами и позиционированием (Потому что отступы — это отношения между элементами, а не их внутреннее дело!)

  • Сохраняем семантическую структуру для сложных компонентов (Чтобы разобраться даже спустя год)

  • Ускоряем разработку благодаря утилитам (И успеваем закончить до дедлайна, что уже чудо)

Личный опыт: спасение из CSS-ада

Знаете, я ведь не просто так все это пишу. Я лично использовал этот подход на реальном коммерческом проекте, когда уже был готов наплевать на все методологии и начать писать стили прямо в атрибутах (да, было настолько х**во).

Когда мой проект вырос до 30+ компонентов, я начал замечать, что:

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

  2. Каждый новый элемент вызывал приступ экзистенциального кризиса: "это новый бл**ь блок или элемент существующего блока?"

  3. Меня начинало потряхивать от необходимости создавать очередной модификатор типа block__element_margin-bottom-md

И тут меня осенило. Если я использую БЭМ для чётких семантических блоков, а Tailwind — для всего остального, жизнь становится прекрасной! Разработка ускорилась примерно в два раза. Я перестал тратить время на бессмысленные споры с самим собой о том, как назвать очередной ёб**ый контейнер, который просто центрирует содержимое.

После перехода на гибридный подход:

  • Верстать стало удовольствием, а не пыткой

  • Компоненты действительно стали переиспользуемыми без тонны модификаторов

  • Код-ревью перестали превращаться в лекции по правильному именованию в БЭМ

  • Дизайнер перестал закатывать глаза, когда я говорил, что внести "небольшие изменения в отступы" займёт полдня

И самое главное — вы сэкономите кучу нервов. Серьезно, это как чёртово освобождение. Просто попробуйте эту ох**нную комбинацию, и вы поймёте, о чём я.

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