Как я проект на БЭМ переводил… и перевел
Сразу хочу заметить, что БЭМ — это далеко не методология на все случаи жизни, и вопрос о необходимости ее применения в том или ином проекте следует рассматривать в частном порядке (в том числе исходя из того, нравится она вам или нет). Также, в силу того, что я не использовал предлагаемую специфическую файловую структуру или генерацию HTML, о них говорить не будем (позднее я все-таки разделил CSS-файл на отдельные части, соответствующие блокам, но этим решил пока ограничиться). Также, уже достаточно много (например, вот и вот) написано о достоинствах и недостатках этого подхода в целом, поэтому говорить об этом тоже не будем, я просто поделюсь своим опытом и размышлениями на эту тему, предполагая, что с сутью вы уже знакомы.
Приступаем
Итак, в моем случае CMS — большой и модульный проект с открытым исходным кодом, в котором каждый модуль может иметь собственные куски бекенда, используя общие и добавляя к ним специфические элементы интерфейса. В идеале, модули должны разрабатываться и другими разработчиками. Думаю, это как раз такой проект, где применение БЭМ (или другой серьезной методологии) является не просто уместным, а необходимым. Особенно это важно, т. к. многие из веб-разработчиков считают CSS побочной, недостойной внимания и глубокого изучения технологией, пытаясь добиться необходимого результата путем копирования готовых кусков разметки и стилей, концентрируясь на том, что им больше нравится, получая в итоге неподдерживаемые таблицы стилей ужасающего качества.
На первых порах проще всего было вообще не трогать исходный код моего проекта, т. к. он достаточно крупный. Вместо этого я создал простой HTML-документ (/index.html) и таблицу стилей (/css/style.css) для него, положил все это на рабочий стол для удобства и решил для начала сверстать несколько фрагментов с картинки выше, используя для этого Notepad++ и браузер. (В результате я хотел получить страницу, содержащую вообще все необходимые мне составные части, и уже затем, в случае успеха, перенести это в свой проект. Упрощенный результат доступен для изучения по ссылке в конце статьи; ссылку на то, как все вышло в реальности, тоже можно глянуть там.)
Кнопки
Я решил начать не со структуры, а с маленького блока кнопки — button. Кнопки у меня бывают 3-х типов: позитивное действие, негативное действие и нейтральное действие. Отличаются они лишь цветом, поэтому эти отличия я описал в виде булевых модификаторов, соответственно, button--positive, button--negative и button--neutral (я выбрал альтернативный синтаксис для модификаторов и вместо одного символа подчеркивания использую два дефиса — для меня это выглядит значительно нагляднее).
В результате в HTML кнопка описывается таким образом:
Также допусти́м и такой вариант (одной из особенностей БЭМ является, в идеале, независимость внешнего вида от используемого тега, хотя я считаю достаточным исходить из того, к каким тегам класс может применяться, и не пытаться предусмотреть все, раздувая таблицу стилей лишними правилами):
Cancel
Выглядит вполне читаемо и понятно. Посмотрим теперь на 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;
}
Эти классы определяют модификаторы для различных типов кнопок (в зависимости от действия) и их состояния при наведении на них курсора мыши.
Посмотрим на наши кнопки вживую:
По-моему, хорошо.
Группы кнопок
В моем проекте я практически нигде не использую кнопки сами по себе, они почти всегда сгруппированы в группы (например, «Сохранить» и «Отмена» в форме). В каждой группе кнопки должны быть расположены горизонтально, на расстоянии ровно в 1 пиксель друг от друга. Чтобы не испытывать затруднений с выдерживанием этого расстояния (в случае с inline- или inline-block-элементами оно зависело бы от форматирования HTML, а именно, от наличия пробела между тегами), проще всего добавить кнопкам правило float: left, но только тогда, когда кнопка является элементом группы кнопок (т. е. само собой было бы неверно добавлять это правило непосредственно блоку button).
Итак, опишем блок группы кнопок buttons с единственным элементом buttons__button, представляющим кнопку, входящую в группу. Тогда HTML группы кнопок будет выглядеть вот так:
Рассмотрим 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, как могло бы показаться), поэтому объявлять классы следует начинать с самых простых блоков, и в самом конце размещать блоки, отвечающие за общую структуру страницы.
Вот как наша группа кнопок выглядит в браузере:
На этом с кнопками мы почти покончили, идем дальше.
Текстовые поля и текстовые области
Далее я решил разобраться с другими элементами управления. Аналогичным образом описал блоки текстового поля text-box и текстовой области text-area (текстовую область рассматривать не будем, т. к. блоки практически идентичны — в исходниках примера можно посмотреть). Далее приведен HTML блока текстового поля. Дополнительно добавлен модификатор text-box--required, означающий, что поле является обязательным к заполнению (он добавляет красную полоску справа от поля):
Соответствующие 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.
Выглядит наше текстовое поле следующим образом:
Поля форм
Как и в случае с кнопками, текстовые поля и области редко применяются сами по себе в моем проекте. Чаще всего они используются в составе форм в виде полей форм (совокупность заголовка и текстового поля, например). Т. е. формы собираются из небольших готовых кусков, а не из отдельных элементов управления. Поэтому я решил добавить блок field, и описать, как ведут себя заголовки и текстовые поля и области внутри поля формы с помощью элементов field__label, field__text-box и field__text-area. В итоге HTML поля формы с текстовой областью выглядит так:
Все просто. Еще раз обратите внимание на порядок следования классов. Сперва, например, следует 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;
}
Этими двумя классами мы задаем размеры для текстовых поля и области, когда они являются элементами поля формы. Результат всего этого следующий:
Также часть полей форм у меня являются локализируемыми (мультиязычными). Им необходим дополнительный визуальный маркер для указания языка, к которому относятся входящие в них текстовые поля или области. В HTML поле формы с набор локализируемых текстовых полей выглядит следующим образом:
en
ru
Обратите внимание на набор классов текстового поля, их четыре. Давайте еще раз по ним пройдемся. Класс 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:
А вот так выглядит его CSS:
.form {
}
.form__field {
margin-top: 10px;
}
.form__buttons {
margin-top: 20px;
}
Как видим, все достаточно очевидно. При необходимости, мы можем вставить, например, группу кнопок в какую-нибудь панель управления на нашем сайте, но если мы говорим о форме, то, снабдив группу кнопок дополнительным классом form__buttons, мы получим необходимый отступ сверху.
В браузере форма целиком выглядит так:
Таблицы
Теперь займемся немного более сложным элементом — таблицей. Думаю, все знают, что таблицы следует верстать таблицами (т. к. это семантически верно и имеет хорошую поддержку браузеров), но, в случае с адаптивной версткой, иногда удобнее все-таки это делать, используя теги общего назначения div со стилями, вроде display: table. В таком случае, на мобильных устройствах горизонтальную таблицу легко превратить в вертикальный список, всячески манипулируя отображаемыми данными (что-то можно скрыть, а что-то — объединить). Как бы там ни было, для реализации таблиц в своем проекте я решил использовать table, но, отчасти в качестве эксперимента, перед этим сверстал ее с использованием div. Прелесть независимости БЭМ от тегов в том, что, заменив затем теги div на table, tr и td, мне ничего не пришлось изменять в своем CSS-файле, таблица выглядела идентично. Я привел оба варианта для сравнения.
Стандартная таблица в HTML выглядит так:
Cell
Cell
Cell
Cell
Cell
Cell
Как видим, каждому тегу дан класс. Может показаться непривычным, зато это дает возможным безболезненно поменять 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. Благодаря этому блок становится относительно независимым от тегов. По сути, повторюсь, не думаю, что есть смысл в этих правилах, если вы уверены, что таблица будет всегда сверстана именно стандартными табличными тегами.
Ну и наконец, посмотрим на результат вживую:
Меню
Переходим к завершающему этапу. Меню представлены блоком menu. Каждое меню может содержать несколько групп элементов меню (элемент menu__group), каждая из которых, в свою очередь, может содержать один заголовок группы элементов меню (элемент menu__group-title) и несколько элементов меню (элемент menu__item). Вот соответствующий HTML:
Я думал над тем, чтобы сделать элементы menu__group и menu__item отдельными блоками, но не нашел аргументов в пользу такого решения: нигде больше они не используются, это привело бы лишь к увеличению количества классов.
Вроде бы все очевидно, но для наглядности приведу еще и CSS:
.menu {
}
.menu__group {
}
.menu__group-title{
}
.menu__item {
display: block;
padding: 5px 0;
}
В данном случае у меня пусты некоторые классы. Как видим, например, внешний вид заголовков групп элементов меню определяется общим блоком sub-title (я на нем не останавливался — посмотрите, пожалуйста, в исходниках). Необходимости в пустых классах нет (скорее, есть необходимость в их удалении). Я их решил оставить для наглядности нашего примера.
Меню само по себе выглядит так:
Общая структура
Напоследок, рассмотрим общую структуру страницы. Но перед этим я хотел бы коснуться еще одного момента. Дело в том, что в основном в статьях по БЭМ рекомендуют не иметь в 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-верстки, отказаться от использования каскадов и так далее. В результате я так или иначе нашел для себя решения, и мне захотелось поделиться этим опытом, чтобы постараться упростить для кого-то этот процесс «привыкания и перестроения», показав еще одну точку зрения.
Методология в целом мне понравилась. Самое важное, на мой взгляд, что она загоняет разработчика в достаточно жесткие рамки в плане структурирования, стиля именования и так далее. В итоге в большинстве случаев есть всего один ответ на вопрос «как?», что очень здорово (особенно, в крупных и командных проектах).
Стану ли я применять БЭМ на мелких и простых проектах? Пока не знаю. Хотя я и не испытывал вообще никаких лишних сложностей и не заметил лишнего «напряга» из-за увеличившегося количества классов, но все-таки следование методологии требует несколько бо́льших усилий, чем при «традиционном» подходе. Хотя, вполне возможно, это из-за недостатка опыта и сноровки.
Надеюсь, было интересно. Вживую посмотреть и потрогать можно тут, а здесь лежит реальная демка проекта, кусок упрощенной админки которого я привел в качестве примера. Там можно глянуть в реальном масштабе как это выглядит, а также, при желании, сравнить с тем, что было раньше.