1. 17. 5. Свод правил БЭМ-а
Чтобы избавиться от сложностей при расширении функциональности, сделать код понятнее, чтобы его легко было поддерживать и делить между всеми участниками команды, в БЭМ придерживаются определённых правил. Ниже перечислим их. Будет много правил, как не делать, но и как делать, мы тоже расскажем.
Правило 1. Не использовать селектор по идентификатору
За счёт идентификатора вы указываете уникальное имя для элемента HTML. Это значит, что и стили элемента будут уникальны и переиспользовать их уже не получится.
В развивающемся проекте (то есть любом, о котором не забудут сразу, как только сделали) постоянно происходит переосмысление внешнего вида и функциональности, и никогда нельзя быть уверенными, что элемент, который сейчас встречается на странице только однажды, завтра не будет использоваться массово.
Даже такая, казалось бы, единичная вещь, как логотип или подвал.
Правило 2. Не использовать селектор по тегу
Разметка элементов страницы может поменяться со временем, к примеру, изменится вложенность разделов, это повлияет на уровень заголовков. Или там, где был просто текст, может появиться блок с подложкой. К тому же количество тегов ограничено, и если привязываться к названию тегов, то придётся переопределять множество стилей для разных элементов сайта, в которых использовались одни и те же теги.
Правило 3. Не использовать универсальный селектор (*)
Есть мнение, что общие стили уменьшают время разработки и сокращают размер кода. Но это не совсем верно. Во-первых, такой селектор влияет на все элементы на странице, это ограничивает перенос CSS-правил в другие проекты, ведь неизвестно как они повлияют на стили этих проектов. Во-вторых, если вы в своём проекте используете сторонние компоненты, то универсальный селектор затронет и их, а это не всегда хорошо.
А что же делать? Какие селекторы использовать?
Только селекторы по классу, написанные в актуальной для проекта нотации и с правильным описанием блоков, элементов и модификаторов.
О том, как правильно называть классы, мы расскажем в разделе «Названия классов по БЭМ».
Правило 4. Не использовать CSS-сброс
Причина, по которой не стоит использовать CSS-сброс, похожа на ту, что описана в пункте про универсальный селектор. CSS-сброс — это набор глобальных CSS-правил, которые созданы для всей страницы. Эти стили влияют на всю разметку целиком, нарушают независимость компонентов и затрудняют их повторное использование.
Ресеты и нормалайзы отменяют и/или переопределяют стили, которые браузер придаёт тегам по умолчанию, а потом эти же стили придётся ещё раз переопределить для того, чтобы результат разработки соответствовал макету.
А что делать?
Определять стили блоков изолированно, так, чтобы сброс отступов по умолчанию, например, у списка определений в подвале, не повлиял бы на список определений в карточках, ведь там могут быть нужны совсем другие отступы.
Правило 5. Не использовать вложенные селекторы
Вложенные селекторы увеличивают связность кода (к примеру, межблочный каскад) и затрудняют повторное использование кода. А ещё такие селекторы повышают специфичность правил, и их сложнее переопределить.
Возможные исключения
Исключением из этого правила будет стилизация для CMS (систем управления контентом), где содержимое блоков генерирует пользователь (редактор, контент-менеджер, комментатор...).
Мы зря учили каскад?! Никогда никакого каскада?
Почти. Использование каскада будет связано с селекторами модификаторов, а также с переопределением браузерных стилей по умолчанию и наследованием, но это — нечастое явление.
Например, у карточки есть вид горизонтальный и вертикальный, и у горизонтального данные, размеченные в список определений, выстроены в ряд, а у вертикального — в колонку. Один из вариантов будет считаться вариантом по умолчанию, второй — вариантом с модификатором, и вот этот вариант с модификатором будет переопределять вариант по умолчанию, как правило, с большей специфичностью (в селекторе правила два класса, значит, это правило специфичнее).
Замечание
Иногда можно услышать, что стили определяются за каскадом. Это сленговое выражение, которое используется для обозначения вложенных селекторов. Например, .content h2
читают как: «за каскадом класса content определим стили для заголовков второго уровня».
Правило 6. Не использовать комбинацию селекторов
К этому типу селекторов относятся селекторы по тегу и классу, например, button.button
, по двум классам, .page.current
и подобные. Такие правила становятся более специфичными, а значит, их сложнее переопределить.
Допустим, у вас есть кнопка:
В стилевом файле вы определяете для неё стили:
У части товаров, которые уже добавлены в избранное, эта кнопка должна быть в состоянии --active
(активная). Для них мы добавляем класс и определяем правило:
Но оно не сработает. Его специфичность ниже правила с базовыми стилями кнопки.
Ещё один пример, где хочется использовать комбинацию селекторов, селекторов по классам, — это выделение активной страницы в пагинации (Pagination). У активной страницы указаны два класса: page
и current
.
Определяем стили активной страницы:
Пока ничего криминального нет. Но если мы решим добавить класс для заблокированного состояния элемента с номером страницы, скажем, disabled
, и текущая страница будет одновременно и активной, в этом случае нам будет сложнее переопределить стили и придётся искусственно утяжелять селектор в правиле.
Если для активной страницы использовать модификатор, то шансы переопределить стили активной страницы выше:
Возможные исключения
В редких случаях методология позволяет комбинировать селекторы тегов и классов. Едва ли не единственный такой случай — это работа с теми материалами CMS, которые формирует пользователь. Ведь пользователь вряд ли будет прописывать классы по БЭМ всем элементам своего текста. Значит, комментарий, который пользователь оставил, нужно стилизовать от селектора по тегу. Тут приходится использовать селекторы p.lead
, button.button
, blockquote.quote--theme-simple
и подобные. Но это скорее вынужденная мера, для которой слишком сложно или тяжеловесно было бы придумывать другое решение.
Псевдоклассы и БЭМ
Использование псевдоклассов, таких как :disabled
, :hover
, :focus
, :checked
, :first-child
, :last-child
, :nth-child
и других, в селекторах увеличивает их специфичность. Любой псевдокласс утяжеляет вес селектора ровно также, как если бы вы использовали комбинированный селектор по двум классам. Такой селектор сложнее переопределить.
Псевдоклассы с нулевой специфичностью
Разберём несколько примеров.
Есть псевдоклассы, которые отвечают за состояния: :hover
, :focus
и подобные. Без них точно не обойтись. Можно, конечно, попробовать добавлять класс через JavaScript. Но зачем так усложнять, если в браузер и CSS заложен этот функционал?
К примеру, для ссылок все используют селекторы с псевдоклассами :hover
, :active
, :focus
.
С состоянием :disabled
(неактивный) немного сложнее, оно не включается автоматически, нужен дополнительный атрибут.
Допустим, в карточке товара есть кнопка В корзину, её состояние (доступна/недоступна) определяется наличием товара в магазине/на складе: товар есть — кнопка доступна, товар отсутствует — кнопка недоступна.
В этом случае доступность кнопки управляется из JavaScript. Но для кнопки поддерживается нативный атрибут disabled
. Если использовать его и селектор :disabled
, то нам как разработчикам делать ничего не нужно, весь функционал на браузере. Браузер видит, что элемент заблокирован, и не даёт ни установить фокус, ни изменить, ни нажать на него и так далее, а также при этом применятся наши стили.
Если бы мы опирались только на класс модификатора, пришлось бы добавлять скрипты, писать проверки, и всё равно была бы вероятность что-то упустить. Допустим, пользователь управляет сайтом не мышкой, а клавиатурой, а это ещё один вид проверок.
Можно использовать и атрибут, и модификатор, главное, синхронно ими управлять (одновременно добавлять/удалять):
Если атрибут disabled
у элемента не поддерживается, а этот функционал нужен, то тут уж без класса-модификатора не обойтись.
Изменить состояние checked
(отмечен/не отмечен) также можно из интерфейса. Достаточно просто кликнуть на соответствующий элемент. Если у вас сложное SPA (Single Page Application) на каком-либо фреймворке и вы переиспользуете этот блок, да ещё и в разных модификациях, то у модификатора преимущество. А если это форма обратной связи на обычном лендинге, менять стили для отмеченного элемента проще с помощью псевдоклассов для состояний. К тому же, как и с :hover
и :focus
, для переключения отмечен/не отмечен можно обойтись вообще без JavaScript.
Порядковые псевдоклассы :first-child
, :last-child
, :nth-child
используют для стилизации отдельных элементов коллекции. Допустим, в таблице чётные строки светлые, нечётные — тёмные.
Проще, когда модификатор родителя, то есть таблицы, определяет эту специфику. Допустим, есть несколько классов-модификаторов для самой таблицы, которые будут делать строки «полосатыми». И да, тут полосатость придётся делать теми самыми осуждаемыми порядковыми селекторами, но таблица — это особый случай.
БЭМ исходит из максимально возможной независимости от контента, его объёма, наличия либо отсутствия и количества. А :first-child
и :last-child
легко ломаются при появлении других потомков — добавили семантику в таблицу (<thead>
, <tbody>
, <tfoot>
) и всё, наши правила не работают.
Например, нужно отобразить насыщенным шрифтом текст в последней строке таблицы, выделить итог.
Разметка таблицы может быть такая:
Используем правило с псевдоклассом :last-child
:
А дальше было решено поменять разметку и добавить семантические теги для шапки, основного содержимого и подвала таблицы, к примеру, так:
После этого правило с :last-child
перестало работать.
Вместо псевдокласса тут лучше использовать модификатор, к примеру, --summary
.
Ещё один кейс, когда стилизация завязана на порядковый номер, — это вертикальное меню, где последний элемент без нижней границы.
Вариант с модификатором будет выглядеть так:
И в этом случае модификатор для последнего пункта меню — это более надёжный способ стилизации. Если поменяется структура меню, селектор .site-navigation__item:last-child
может перестать срабатывать, в отличие от модификатора. Стили уже не будут зависеть от разметки. Если блок встречается где-то ещё и с некоторыми визуальными отличиями, не придётся искусственно усложнять селектор, чтобы переопределить стили.
Есть ситуации, когда модификатор только усложнит реализацию компонента. Допустим, карточка объекта недвижимости (или любая другая карточка товара), где характеристики разделяются декоративными элементами (к примеру, точками).
Например, так:
Все вышеперечисленные примеры показывают, что нет чётких правил для принятия решения «псевдокласс vs модификатор». Многое зависит от типа псевдокласса, от того, что это за проект (пара-тройка страниц или большой сервис), собираетесь ли вы его поддерживать или «сделали и забыли», будет ли переиспользоваться этот элемент в проекте, будет ли элемент видоизменяться.
Как побороть специфичность правил с псевдоклассами
Чтобы правило с псевдоклассом в ненужный момент не переопределяло свойства модификатора блока/элемента, придётся искусственно увеличивать специфичность правил. Мы нашли несколько способов, которые и поднимают специфичность, и не затрудняют понимание кода.
Способ 1. Удваивание названия класса модификатора в правиле.
Способ 2. Добавление к классу модификатора класса базового блока/элемента.
Способ 3 (от разработчиков БЭМа). Добавление селектора по атрибуту class
.
Правило 7. Не использовать селектор по атрибуту
Такие правила не информативны, и неясно, к какому блоку/элементу они относятся.
К примеру, у нас есть форма поиска в шапке сайта:
Используем селектор атрибутов для написания стилей формы:
По имени селектора сложно сказать, что стили относятся к форме поиска. Использование классов, к чему и призывает БЭМ, делает код более понятным. Ведь класс — это единственный селектор, который позволяет изолировать стили каждого компонента в проекте, улучшить читаемость кода и не ограничивать повторное использование самих компонентов.
Этот пример можно переписать, допустим, так:
Иногда селекторы по атрибутам всё же добавляют в правила, к примеру, селекторы по атрибуту href
.
Для активной ссылки в хлебных крошках определить отличный цвет текста и изменить вид курсора на обычный можно так:
Или актуальные сейчас aria-
атрибуты. В этот же пример с хлебными крошками можно добавить доступности и переписать так:
И кстати, сами авторы БЭМа дают добро на такое использование селекторов по атрибутам.
Правило 8. Не задавать внешнюю геометрию блокам
Блок, он же компонент, должен быть изолированным. Это означает, что все свои отступы он должен хранить внутри себя, а не «давить» ими на другие блоки.
Из этого выводится запрет на внешние отступы у блоков. margin
— запрещённое свойство для блоков в БЭМ. Внешние отступы могут быть у элементов блока, если это нужно. А у самого блока — только внутренние.
Например, мы назначаем слайдер блоком. У слайдера внутри есть ещё компонент-кнопка, которая в данной ситуации также является элементом слайдера (микс). И у кнопки есть отступы снаружи, она явно отталкивается от других элементов внутри слайдера. Отступы нужно задать элементу слайдера, а не блоку-кнопке, иначе во всех других ситуациях использования у кнопки также будут внешние отступы, которые ей совсем не нужны. То же и со слайдером: вероятно, у него будут внутренние отступы, а внешние будут заданы его обёртке — элементу более крупного блока.
Замечание
Если блок состоит из элементов, важно, чтобы элементы не «вываливались» из родительского блока. К примеру, у вас есть список карточек, они выстраиваются в одну колонку, чтобы задать расстояние между карточками, и вы используете margin-bottom
. Если у блока не предусмотрены внутренние отступы, есть шанс, что последний элемент выпадет из блока со списком карточек (речь о блочном контексте, в частности «Выпадание» внешних отступов). Можно было бы последнему элементу установить индивидуальный отступ снизу, равный 0, допустим, так: .catalog__item:last-child { margin-bottom: 0; }
, но БЭМ не одобряет селекторы по псевдоклассам (читайте выше в заметке «А что насчёт псевдоклассов в БЭМ»). Подходящих решений в этом случае несколько, допустим, это может быть сетка на CSS Grid Layout.
Правило 9. Не стараться определить все стили в одном CSS правиле (шрифты, сетка, декор, отступы)
Не следует на один и тот же блок навешивать стили, касающиеся всего вообще. Такие блоки ещё называют «жадными». По возможности лучше дифференцировать, это сделает код прозрачнее.
Ещё один пример с кнопками, представим, что у нас есть темы для кнопок. Их разметка:
Вы решили сэкономить и описали все правила для кнопки с темой в одном правиле с селектором .button.button--theme-islands
. Затем нужно реализовать состояние Кнопка активна, вы добавляете новый класс для блока button--active
, и теперь, чтобы переопределить свойства кнопки, придётся написать селектор .button.button--active {}
, а это не очень хорошо (искусственно усложняется селектор).
Лучше определить базовые стили для класса button
и дополнить стили для темы и активной кнопки в отдельных правилах без комбинирования селекторов.
Все эти правила получены в результате набивания шишек на одних и тех же проблемах. И этот список может расширяться и дополняться командами, которые выбрали БЭМ в качестве методологии вёрстки на своём проекте.
Бывают ситуации, когда какое-то из вышеперечисленных правил может быть нарушено. Но для исключения должны быть веские причины и чёткое понимание, зачем это делается. Возможно, по-другому задачу никак не решить. Можно обосновать даже наличие правила с !important
в стилях — большой проект, куча сторонних библиотек и компонентов. Но, как вы сами понимаете, это плохая практика, и рано или поздно такое решение «выстрелит в ногу».
В частности:
Single responsibility — принцип единственной ответственности для БЭМ-сущностей, чтобы не получилось, что один класс определяет кучу CSS свойств (внешний вид, позиционирование, раскладка). Блок отвечает за что-то одно, например, за сетку. У блока должна быть только одна причина для изменения.
Open-closed — принцип открытости/закрытости, с помощью него запрещается переопределение свойств БЭМ сущностей, также этот принцип показывает, что мы верно выбрали базовый класс, который затем при необходимости дополняем.
Используют принцип DRY (Don’t Repeat Yourself, «не повторяйся»), который спасает от бездумного дублирования кода.
Не обходится и без традиционного KISS (Keep It Simple, Stupid, «делай проще, тупица»), ведь всегда чем проще, тем лучше и понятнее.
Получается, что свёрстанный по БЭМ сайт — это абсолютно плоская система, в которой в отдельных модулях будут расписаны стили для отдельных блоков. Эти блоки можно будет использовать в других местах на сайте вне зависимости от содержимого. А ещё вы наверняка будете знать, где лежат стили каждой конкретной детали, если их нужно будет поменять или поправить, и не нужно будет изворачиваться и что-то придумывать, чтобы переопределить стили, ведь «лишнего» каскада в системе не будет.
Last updated