Как я проект на БЭМ переводил… и перевел

в 10:27, , рубрики: bem, css, html, БЭМ, верстка

Связка HTML и CSS (CSS в большей степени) всегда казалась мне несколько «туманной», хуже всего поддающейся контролю и тестированию. Я придумывал для себя различные правила и пытался так или иначе стандартизировать свой подход, но не было ощущения, что «вот, это оно». Я несколько раз мельком знакомился с БЭМ (и не только), читал статьи на эту тему, но дальше чтения дело не заходило. Но чем дальше, тем сильнее было ощущение необходимости в наличии определенной строгой методологии. В конце концов, я решил попробовать внедрить БЭМ на одном из своих проектов, где без этого, на мой взгляд, было не обойтись. Речь идет о CMS, упрощенную страничку бекенда которой я приведу в качестве примера верстки:

Как я проект на БЭМ переводил… и перевел - 1

Сразу хочу заметить, что БЭМ — это далеко не методология на все случаи жизни, и вопрос о необходимости ее применения в том или ином проекте следует рассматривать в частном порядке (в том числе исходя из того, нравится она вам или нет). Также, в силу того, что я не использовал предлагаемую специфическую файловую структуру или генерацию HTML, о них говорить не будем (позднее я все-таки разделил CSS-файл на отдельные части, соответствующие блокам, но этим решил пока ограничиться). Также, уже достаточно много (например, вот и вот) написано о достоинствах и недостатках этого подхода в целом, поэтому говорить об этом тоже не будем, я просто поделюсь своим опытом и размышлениями на эту тему, предполагая, что с сутью вы уже знакомы.

Приступаем

Итак, в моем случае CMS — большой и модульный проект с открытым исходным кодом, в котором каждый модуль может иметь собственные куски бекенда, используя общие и добавляя к ним специфические элементы интерфейса. В идеале, модули должны разрабатываться и другими разработчиками. Думаю, это как раз такой проект, где применение БЭМ (или другой серьезной методологии) является не просто уместным, а необходимым. Особенно это важно, т. к. многие из веб-разработчиков считают CSS побочной, недостойной внимания и глубокого изучения технологией, пытаясь добиться необходимого результата путем копирования готовых кусков разметки и стилей, концентрируясь на том, что им больше нравится, получая в итоге неподдерживаемые таблицы стилей ужасающего качества.

На первых порах проще всего было вообще не трогать исходный код моего проекта, т. к. он достаточно крупный. Вместо этого я создал простой HTML-документ (/index.html) и таблицу стилей (/css/style.css) для него, положил все это на рабочий стол для удобства и решил для начала сверстать несколько фрагментов с картинки выше, используя для этого Notepad++ и браузер. (В результате я хотел получить страницу, содержащую вообще все необходимые мне составные части, и уже затем, в случае успеха, перенести это в свой проект. Упрощенный результат доступен для изучения по ссылке в конце статьи; ссылку на то, как все вышло в реальности, тоже можно глянуть там.)

Кнопки

Я решил начать не со структуры, а с маленького блока кнопки — button. Кнопки у меня бывают 3-х типов: позитивное действие, негативное действие и нейтральное действие. Отличаются они лишь цветом, поэтому эти отличия я описал в виде булевых модификаторов, соответственно, button--positive, button--negative и button--neutral (я выбрал альтернативный синтаксис для модификаторов и вместо одного символа подчеркивания использую два дефиса — для меня это выглядит значительно нагляднее).

В результате в HTML кнопка описывается таким образом:

<button class="button button--positive" type="button">Text</button>

Также допусти́м и такой вариант (одной из особенностей БЭМ является, в идеале, независимость внешнего вида от используемого тега, хотя я считаю достаточным исходить из того, к каким тегам класс может применяться, и не пытаться предусмотреть все, раздувая таблицу стилей лишними правилами):

<a class="button button--neutral" href="#">Cancel</a>

Выглядит вполне читаемо и понятно. Посмотрим теперь на CSS:

.button {
  border: none;
  cursor: pointer;
  font: normal 15px 'PT Sans', sans-serif;
  line-height: 20px;
  display: inline-block;
  padding: 5px 10px;
}

Кнопка описана очень просто. Хотя я и встречал рекомендации сбрасывать значения всех правил внутри своих классов (чтобы на них не влияло окружение), но, как по мне, это уже слишком, и требуется лишь тогда, когда есть реальный шанс, что вы будете повторно использовать ваш блок в каком-то другом проекте, где стиль верстки отличается от вашего (например, вы разрабатываете какой-то виджет, который нельзя вставить как iframe в целевую страницу). Класс блока button, как и требует БЭМ, никаким образом не специфицирует свои размеры или внешние отступы.

Идем далее:

.button--positive {
  background-color: #87b919;
  color: #fff;
}

.button--positive:hover {
  background-color: #a0d71e;
  color: #fff;
}

.button--negative {
  background-color: #ff4100;
  color: #fff;
}

.button--negative:hover {
  background-color: #ff7346;
  color: #fff;
}

.button--neutral {
  background-color: #f0f0f0;
}

.button--neutral:hover {
  background-color: #f5f5f5;
}

Эти классы определяют модификаторы для различных типов кнопок (в зависимости от действия) и их состояния при наведении на них курсора мыши.

Посмотрим на наши кнопки вживую:

Как я проект на БЭМ переводил… и перевел - 2

По-моему, хорошо.

Группы кнопок

В моем проекте я практически нигде не использую кнопки сами по себе, они почти всегда сгруппированы в группы (например, «Сохранить» и «Отмена» в форме). В каждой группе кнопки должны быть расположены горизонтально, на расстоянии ровно в 1 пиксель друг от друга. Чтобы не испытывать затруднений с выдерживанием этого расстояния (в случае с inline- или inline-block-элементами оно зависело бы от форматирования HTML, а именно, от наличия пробела между тегами), проще всего добавить кнопкам правило float: left, но только тогда, когда кнопка является элементом группы кнопок (т. е. само собой было бы неверно добавлять это правило непосредственно блоку button).

Итак, опишем блок группы кнопок buttons с единственным элементом buttons__button, представляющим кнопку, входящую в группу. Тогда HTML группы кнопок будет выглядеть вот так:

<div class="buttons">
  <button class="buttons__button button button--positive" type="button">Send</button>
  <a class="buttons__button button button--neutral" href="#">Cancel</a>
</div>

Рассмотрим CSS:

.buttons {
}

Класс блока buttons пуст.

.buttons::after {
  content: '';
  display: block;
  clear: both;
}

Поскольку к кнопкам внутри группы будет применяться правило float: left (причину я описал выше), я отменяю обтекание таким образом. Кстати, этот способ закрытия потока обтекания float нравится мне больше всего, хотя в устаревших браузерах он и не будет работать (чаще всего ориентироваться на них нет необходимости). В любом случае, не в этом суть.

.buttons__button {
  float: left;
  margin-right: 1px;
}

Здесь мы непосредственно описываем элемент-кнопку, входящую в группу, с отступом в одну точку справа.

.buttons__button:last-child {
  margin: 0;
}

Последняя кнопка в группе не должна иметь отступа справа, используем псевдокласс :last-child для этого. Я использовал именно псевдокласс, а не модификатор, т. к. все без исключения кнопки в группах, если они расположены последними, не должны иметь этот отступ справа. Считаю использование модификаторов в таком случае излишним.

На мой взгляд, получается достаточно здорово. Сами по себе блоки никак не позиционируют себя, не описывают внешних отступов. Но когда мы помещаем блок в другой блок, он как бы одновременно становится элементом своего блока и именно класс элемента позволяет дополнительно специфицировать все необходимые правила его расположения, если они необходимы. Кстати, я всегда располагаю классы элемента первыми, затем следуют классы модификаторов элемента, а уже затем — классы блока и его модификаторов. Это очень упрощает чтение HTML, т. к. если классов много, то сразу понятнее, что во что входит. Еще момент (на всякий случай). Порядок применения CSS-классов определяется порядком их следования в CSS-файле (а не в атрибуте class, как могло бы показаться), поэтому объявлять классы следует начинать с самых простых блоков, и в самом конце размещать блоки, отвечающие за общую структуру страницы.

Вот как наша группа кнопок выглядит в браузере:

Как я проект на БЭМ переводил… и перевел - 3

На этом с кнопками мы почти покончили, идем дальше.

Текстовые поля и текстовые области

Далее я решил разобраться с другими элементами управления. Аналогичным образом описал блоки текстового поля text-box и текстовой области text-area (текстовую область рассматривать не будем, т. к. блоки практически идентичны — в исходниках примера можно посмотреть). Далее приведен HTML блока текстового поля. Дополнительно добавлен модификатор text-box--required, означающий, что поле является обязательным к заполнению (он добавляет красную полоску справа от поля):

<input class="text-box text-box--required" type="text" />

Соответствующие CSS-классы выглядят так:

.text-box {
  background-color: #f0f0f0;
  border: none;
  font: normal 15px 'PT Sans', sans-serif;
  line-height: 20px;
  outline: none;
  padding: 5px 10px;
  resize: none;
}

.text-box:hover {
  background-color: #f5f5f5;
}

.text-box:focus {
  background-color: #f5f5f5;
}

.text-box--required {
  border-right: 5px solid #ff4100;
}

Ничего особенного здесь нет, за исключением, повторюсь, последнего модификатора text-box--required. У текстовой области тоже есть такой, но называется он text-area--required.

Выглядит наше текстовое поле следующим образом:

Как я проект на БЭМ переводил… и перевел - 4

Поля форм

Как и в случае с кнопками, текстовые поля и области редко применяются сами по себе в моем проекте. Чаще всего они используются в составе форм в виде полей форм (совокупность заголовка и текстового поля, например). Т. е. формы собираются из небольших готовых кусков, а не из отдельных элементов управления. Поэтому я решил добавить блок field, и описать, как ведут себя заголовки и текстовые поля и области внутри поля формы с помощью элементов field__label, field__text-box и field__text-area. В итоге HTML поля формы с текстовой областью выглядит так:

<div class="field">
  <label class="field__label label">Body</label>
  <textarea class="field__text-area text-area"></textarea>
</div>

Все просто. Еще раз обратите внимание на порядок следования классов. Сперва, например, следует field__label, а label — после него, т. к. тег label является в первую очередь элементом field__label своего блока field, а уже потом независимым блоком label. Такое единообразие очень помогает. Рассмотрим CSS:

.field {
}

Этот класс пуст. При отображении полей форм непосредственно в формах нам потребуется, чтобы между ними были вертикальные отступы, но мы опишем это в соответствующем элементе form__field блока form далее.

.field__label {
  display: block;
  margin-bottom: 1px;
}

Заголовки внутри блока field будут выводиться с новой строки и иметь отступ в один пиксель снизу.

.field__text-box {
  width: 430px;
}

.field__text-area {
  width: 430px;
  height: 190px;
}

Этими двумя классами мы задаем размеры для текстовых поля и области, когда они являются элементами поля формы. Результат всего этого следующий:

Как я проект на БЭМ переводил… и перевел - 5

Также часть полей форм у меня являются локализируемыми (мультиязычными). Им необходим дополнительный визуальный маркер для указания языка, к которому относятся входящие в них текстовые поля или области. В HTML поле формы с набор локализируемых текстовых полей выглядит следующим образом:

<div class="field">
  <label class="field__label label">Subject</label>
  <div class="field__culture">
    <div class="field__culture-flag">en</div>
  </div>
  <input class="field__text-box field__text-box--multilingual text-box text-box--required" type="text" />
  <div class="field__multilingual-separator"></div>
  <div class="field__culture">
    <div class="field__culture-flag">ru</div>
  </div>
  <input class="field__text-box field__text-box--multilingual text-box text-box--required" type="text" />
</div>

Обратите внимание на набор классов текстового поля, их четыре. Давайте еще раз по ним пройдемся. Класс field__text-box определяет размеры текстового поля внутри поля формы, field__text-box--multilingual добавляет небольшой дополнительный отступ справа, чтобы символы при наборе не залезали под маркер языка, который отображается поверх текстового поля. Класс text-box определяет основные параметры текстового поля, а text-box--required добавляет красную полоску справа от поля.

Новые CSS-классы:

.field__culture {
  position: relative;
  left: 450px;
  width: 0;
  z-index: 10;
}

.field__culture-flag {
  background-color: #323232;
  color: #fff;
  cursor: default;
  font-size: 8px;
  line-height: 16px;
  text-align: center;
  text-transform: uppercase;
  position: absolute;
  left: -23px;
  top: 7px;
  width: 16px;
  height: 16px;
}

.field__multilingual-separator {
  height: 1px;
}

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

Формы

Теперь, рассмотрим блок формы form. Формы состоят из полей форм и групп кнопок, которые у нас уже описаны, но добавляют к ним вертикальные отступы с помощью классов элементов form__field и form__buttons. Вот так выглядит упрощенный HTML блока form:

<form class="form">
  <div class="form__field field">
    <label class="field__label label">Body</label>
    <textarea class="field__text-area text-area"></textarea>
  </div>
  <div class="form__buttons buttons">
    <button class="buttons__button button button--positive" type="button">Send</button>
    <a class="buttons__button button button--neutral" href="#">Cancel</a>
  </div>
</form>

А вот так выглядит его CSS:

.form {
}

.form__field {
  margin-top: 10px;
}

.form__buttons {
  margin-top: 20px;
}

Как видим, все достаточно очевидно. При необходимости, мы можем вставить, например, группу кнопок в какую-нибудь панель управления на нашем сайте, но если мы говорим о форме, то, снабдив группу кнопок дополнительным классом form__buttons, мы получим необходимый отступ сверху.

В браузере форма целиком выглядит так:

Как я проект на БЭМ переводил… и перевел - 6

Таблицы

Теперь займемся немного более сложным элементом — таблицей. Думаю, все знают, что таблицы следует верстать таблицами (т. к. это семантически верно и имеет хорошую поддержку браузеров), но, в случае с адаптивной версткой, иногда удобнее все-таки это делать, используя теги общего назначения div со стилями, вроде display: table. В таком случае, на мобильных устройствах горизонтальную таблицу легко превратить в вертикальный список, всячески манипулируя отображаемыми данными (что-то можно скрыть, а что-то — объединить). Как бы там ни было, для реализации таблиц в своем проекте я решил использовать table, но, отчасти в качестве эксперимента, перед этим сверстал ее с использованием div. Прелесть независимости БЭМ от тегов в том, что, заменив затем теги div на table, tr и td, мне ничего не пришлось изменять в своем CSS-файле, таблица выглядела идентично. Я привел оба варианта для сравнения.

Стандартная таблица в HTML выглядит так:

<table class="table">
  <tr class="table__row">
    <th class="table__cell table__cell--header">Cell</th>
    <th class="table__cell table__cell--header">Cell</th>
    <th class="table__cell table__cell--header">Cell</th>
  </tr>
  <tr class="table__row">
    <td class="table__cell">Cell</td>
    <td class="table__cell">Cell</td>
    <td class="table__cell">Cell</td>
  </tr>
</table>

Как видим, каждому тегу дан класс. Может показаться непривычным, зато это дает возможным безболезненно поменять table, tr и td на div и не визуально не заметить различия.

CSS таблицы:

.table {
  border-collapse: collapse;
  display: table;
  width: 100%;
}

.table__row {
  display: table-row;
}

.table__cell {
  font-weight: normal;
  text-align: left;
  vertical-align: top;
  display: table-cell;
  padding: 5px 10px;
}

.table__row:hover .table__cell {
  background: #ffff96;
}

.table__cell--header {
  background: #f0f0f0;
}

.table__row:hover .table__cell--header {
  background: #f0f0f0;
}

Как видим, для самого блока, как и для его элементов, установлены правила display: table, display: table-row и display: table-cell. Благодаря этому блок становится относительно независимым от тегов. По сути, повторюсь, не думаю, что есть смысл в этих правилах, если вы уверены, что таблица будет всегда сверстана именно стандартными табличными тегами.

Ну и наконец, посмотрим на результат вживую:

Как я проект на БЭМ переводил… и перевел - 7

Меню

Переходим к завершающему этапу. Меню представлены блоком menu. Каждое меню может содержать несколько групп элементов меню (элемент menu__group), каждая из которых, в свою очередь, может содержать один заголовок группы элементов меню (элемент menu__group-title) и несколько элементов меню (элемент menu__item). Вот соответствующий HTML:

<div class="menu">
  <div class="menu__group">
    <div class="menu__group-title sub-title">
      Group title 1
    </div>
    <a class="menu__item" href="#">Menu item 1</a>
    <a class="menu__item" href="#">Menu item 2</a>
    <a class="menu__item" href="#">Menu item 3</a>
  </div>
</div>

Я думал над тем, чтобы сделать элементы menu__group и menu__item отдельными блоками, но не нашел аргументов в пользу такого решения: нигде больше они не используются, это привело бы лишь к увеличению количества классов.

Вроде бы все очевидно, но для наглядности приведу еще и CSS:

.menu {
}

.menu__group {
}

.menu__group-title{
}

.menu__item {
  display: block;
  padding: 5px 0;
}

В данном случае у меня пусты некоторые классы. Как видим, например, внешний вид заголовков групп элементов меню определяется общим блоком sub-title (я на нем не останавливался — посмотрите, пожалуйста, в исходниках). Необходимости в пустых классах нет (скорее, есть необходимость в их удалении). Я их решил оставить для наглядности нашего примера.

Меню само по себе выглядит так:

Как я проект на БЭМ переводил… и перевел - 8

Общая структура

Напоследок, рассмотрим общую структуру страницы. Но перед этим я хотел бы коснуться еще одного момента. Дело в том, что в основном в статьях по БЭМ рекомендуют не иметь в CSS-файле правил, не относящихся к блокам. Т. е. общих правил, применимых ко всему документу (например, с селектором по тегу, а не по классу). Я решил не идти этим путем, т. к. в таком случае увеличивается количество правил, которые необходимо дублировать в каждом блоке или элементе. Я не вижу особой причины делать это в моем случае. Если задуматься, то все блоки в моем проекте описываются в рамках единого контекста, и он неизменяем, поэтому вполне допустимо, например, задать общий стиль для текста, и во всех блоках отталкиваться от него, т. к. они все-равно должны иметь обобщенный стиль.

Кроме того, мне кажется лишним назначать класс для каждого заголовка, абзаца в тексте, каждой ссылки. В таком случае, при использовании, например, WYSIWYG-редактора нам потребовалось бы добавлять эти классы вручную (или делать это автоматически при сохранении). Так или иначе, это лишнее неудобство.

Вернемся к общей структуре. Я решил представить ее одним блоком master-detail с двумя основными элементами: master-detail__master и master-detail__detail, отвечающими, соответственно, за левую-темную и правую-светлую части страницы.

В master-detail__master я добавил два меню. Одно меню не содержит никаких дополнительных классов элемента master-detail__master, т. к. нет нужды дополнять его какими-то CSS-правилами. Второе же меню является одновременно элементом master-detail__secondary-menu, что позиционирует его внизу элемента master-detail__master. Дополнительно, элементы этого второго меню «замиксованы» с элементом master-detail__secondary-menu-item, что придает им серый цвет.

Не буду приводить HTML/CSS этого блока, т. к. он слишком громоздкий, и его необходимо рассматривать в контексте остального содержимого страницы. Поэтому, предлагаю взглянуть на исходники тестового примера, ссылку на которые можно найти ниже.

Также на странице остался еще один блок — табы. Решил, что описывать их уже не имеет смысла, т. к. блок очень прост.

Ну что же, в итоге мы получим такой результат, как на первой картинке.

Выводы

Зачем я решил написать это? Когда я принялся разбираться с БЭМ у меня было много вопросов, на которые я не находил однозначных ответов. Это был некий полуфабрикат идеи. Было интересно перестроиться и взглянуть по-новому на процесс HTML-верстки, отказаться от использования каскадов и так далее. В результате я так или иначе нашел для себя решения, и мне захотелось поделиться этим опытом, чтобы постараться упростить для кого-то этот процесс «привыкания и перестроения», показав еще одну точку зрения.

Методология в целом мне понравилась. Самое важное, на мой взгляд, что она загоняет разработчика в достаточно жесткие рамки в плане структурирования, стиля именования и так далее. В итоге в большинстве случаев есть всего один ответ на вопрос «как?», что очень здорово (особенно, в крупных и командных проектах).

Стану ли я применять БЭМ на мелких и простых проектах? Пока не знаю. Хотя я и не испытывал вообще никаких лишних сложностей и не заметил лишнего «напряга» из-за увеличившегося количества классов, но все-таки следование методологии требует несколько бо́льших усилий, чем при «традиционном» подходе. Хотя, вполне возможно, это из-за недостатка опыта и сноровки.

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

Автор: DmitrySikorsky

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js