Укрощение Cascade с помощью БЭМ и современных селекторов CSS
Как, иногда кажется что все технологии в мире фронтенд-разработки, в том числе написание CSS в формате БЭМ (writing CSS in a BEM format) могут вызывать противоречивые чувства. Но это — по крайней мере, в моем Twitter пузыре — БЭМ одна из самых популярных методологий CSS.
Лично я думаю, что БЭМ очень хорошая методология, и что все непременно должны ее использовать. Но я также признаю причины, по которым это иногда не возможно.
Независимо от вашего мнения о БЭМ, он предлагает несколько преимуществ, самым большим из которых является то, что он помогает избежать конфликтов специфичности в CSS Cascade. Это потому, что при правильном использовании любые селекторы, написанные в формате БЭМ, должны иметь одинаковую оценку специфичности (0,1,0). Я разработал CSS для множества крупных веб-сайтов на протяжении многих лет (это были правительственные проекты, университетские и банковские), и именно в этих более крупных проектах я обнаружил, что БЭМ действительно очень полезен. Написание CSS намного веселее, когда вы уверены, что стили, которые вы пишете или редактируете, не влияют на какую-либо другую часть сайта.
На самом деле есть исключения, когда добавление специфичности считается вполне приемлемым. Например: псевдоклассы :hover и :focus . Они имеют показатель специфичности 0,2,0. Другой – псевдоэлементы, например ::before и ::after , которые имеют показатель специфичности 0, 1, 1. Однако в остальной части этой статьи давайте предположим, что нам не нужна какая-либо другая специфичность. 🤓
Но на самом деле я написал эту статью не для того, чтобы продавать вам БЭМ. Вместо этого я хочу поговорить о том, как мы можем использовать его вместе с современными селекторами CSS — например, :is(), :has(), :where() и т. д. — чтобы получить еще больший контроль над Cascade.
Что такого в современных селекторах CSS?
Спецификация CSS Selectors Level 4 (CSS Selectors Level 4 spec) дает нам несколько новых мощных способов выбора элементов. Некоторые из моих любимых функций включают :is()
, :where()
и :not()
каждая из которых поддерживается всеми современными браузерами и в настоящее время безопасна для использования практически в любом проекте.
:is() и :where() в основном одно и то же, за исключением того, как они влияют на специфичность. В частности, :where() всегда имеет показатель специфичности 0,0,0. Да, даже :where(button#widget.some-class) не имеет никакой специфичности. Между тем, специфичность :is() — это элемент в списке аргументов с наивысшей специфичностью. Итак, у нас уже есть различие между двумя современными селекторами, с которым мы можем работать.
Невероятно мощный реляционный псевдокласс :has() также быстро получает поддержку браузеров (и, по моему скромному мнению, это самая большая новая функция CSS со времен Grid). Однако на момент написания статьи поддержка :has() в браузерах еще недостаточно хороша для использования в рабочей среде.
Рассмотрим пример использования одного из этих псевдоклассов в БЭМ и…
/* ❌ specificity score: 0,2,0 */ .something:not(.something--special) { /* styles for all somethings, except for the special somethings */ }
Упс! Видите этот показатель специфичности? Помните, что в БЭМ мы в идеале хотим, чтобы все наши селекторы имели показатель специфичности 0,1,0. Почему 0,2,0 плохо? Рассмотрим аналогичный пример, более подробно:
.something:not(a) { color: red; } .something--special { color: blue; }
Несмотря на то, что второй селектор является последним в исходном порядке, побеждает более высокая специфичность первого селектора (0,1,1), и цвет .something—special элементов будет установлен на красный. Так как, если ваш БЭМ написан правильно и к выбранному элементу в HTML применены как базовый класс .something, так и специальный класс-модификатор .something--special
.
При неосторожном использовании эти псевдоклассы могут неожиданным образом повлиять на Cascade. И именно такие несоответствия могут создать головную боль в будущем, особенно при работе с большими и сложными кодовыми базами.
Ну что теперь?
Помните, что я говорил о :where() и о том, что его специфичность равна нулю? Мы можем использовать это в своих интересах:
/* ✅ specificity score: 0,1,0 */ .something:where(:not(.something--special)) { /* etc. */ }
Первая часть этого селектора (.something) получает свой обычный показатель специфичности 0,1,0. Но :where() — и все, что внутри него — имеет специфичность 0, что не увеличивает специфичность селектора.
:where() позволяет нам вложенность
Люди, которые не так сильно заботятся о специфичности, как и я (а таких людей, если честно, наверное, много), неплохо справлялись с вложенностью. С некоторыми беззаботными движениями клавиатуры мы можем получить такой CSS (обратите внимание, что для краткости я использую Sass):
.card { ... } .card--featured { /* etc. */ .card__title { ... } .card__title { ... } } .card__title { ... } .card__img { ... }
В этом примере у нас есть компонент .card. Когда это «популярная» карта (используется класс .card—featured), заголовок и изображение карты должны быть оформлены по-другому. Но, как мы теперь знаем, приведенный выше код приводит к показателю специфичности, несовместимому с остальной частью нашей системы.
Заядлый знаток специфичности мог бы сделать следующее:
.card { ... } .card--featured { ... } .card__title { ... } .card__title--featured { ... } .card__img { ... } .card__img--featured { ... }
Это не так уж и плохо, верно? Честно говоря, это красивый CSS.
Однако в HTML есть обратная сторона. Опытные авторы БЭМ, вероятно, до боли знакомы с неуклюжей логикой шаблонов, необходимой для условного применения классов модификаторов к нескольким элементам. В этом примере шаблон HTML должен условно добавить класс модификатора —featured к трем элементам (.card, .card__title и .card__img), хотя, возможно, даже больше в реальном примере.
Селектор :where() может помочь нам написать гораздо меньше шаблонной логики и меньше классов БЭМ для загрузки, не повышая уровень специфичности.
.card { ... } .card--featured { ... } .card__title { ... } :where(.card--featured) .card__title { ... } .card__img { ... } :where(.card--featured) .card__img { ... }
Вот то же самое, но в Sass (обратите внимание на амперсанды в конце):
.card { ... } .card--featured { ... } .card__title { /* etc. */ :where(.card--featured) & { ... } } .card__img { /* etc. */ :where(.card--featured) & { ... } }
Следует ли вам выбрать этот подход вместо применения классов модификаторов к различным дочерним элементам, зависит от личных предпочтений. Но, по крайней мере с :where() сейчас у нас есть выбор!
Как насчет не-БЭМ HTML?
Мы не живем в идеальном мире. Иногда вам нужно иметь дело с HTML, который находится вне вашего контроля. Например, сторонний скрипт, который внедряет HTML, который вам нужно стилизовать. Эта разметка часто не написана с именами классов БЭМ. В некоторых случаях эти стили используют не классы, а идентификаторы!
И снова :where() поддерживает нас. Это решение немного хакерское, так как нам нужно сослаться на класс элемента где-то выше по дереву DOM, о существовании которого мы знаем.
/* ❌ specificity score: 1,0,0 */ #widget { /* etc. */ } /* ✅ specificity score: 0,1,0 */ .page-wrapper :where(#widget) { /* etc. */ }
Однако ссылка на родительский элемент кажется немного рискованной и ограничительной. Что, если этот родительский класс изменится или по какой-то причине его не будет? Лучшим (но, возможно, столь же хакерским) решением было бы использовать :is() вместо этого. Помните, что специфичность :is() равна самому специфичному селектору в его списке селекторов.
Таким образом, вместо ссылки на класс, о существовании которого мы знаем (или надеемся!) с помощью :where(), как в приведенном выше примере, мы могли бы сослаться на созданный класс и тег <body>
.
/* ✅ specificity score: 0,1,0 */ :is(.dummy-class, body) :where(#widget) { /* etc. */ }
Вездесущее body
поможет нам выбрать наш элемент #widget, а наличие класса .dummy-class внутри того же :is() дает селектору body
ту же оценку специфичности, что и класс (0,1,0)… а использование :where() гарантирует, что селектор не станет более специфичным.
Заключение
В статье я постарался описать то как мы можем использовать современные функции управления специфичностью псевдоклассов :is() и :where() наряду с предотвращением конфликтов специфичности, которые мы получаем при написании CSS в формате BEM. И в не столь отдаленном будущем, когда :has() получит поддержку Firefox (в настоящее время она поддерживается с пометкой на момент написания статьи), мы, вероятно, захотим соединить ее с :where(), чтобы отменить ее специфичность.
Независимо от того, идете ли вы ва-банк в использование БЭМ-именовании или нет, я надеюсь, что мы можем согласиться с тем, что постоянство в специфичности селекторов — это хорошо!