Укрощение Cascade с помощью БЭМ и современных селекторов CSS

Spread the love

Как, иногда кажется что все технологии в мире фронтенд-разработки, в том числе написание 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(), чтобы отменить ее специфичность.

Независимо от того, идете ли вы ва-банк в использование БЭМ-именовании или нет, я надеюсь, что мы можем согласиться с тем, что постоянство в специфичности селекторов — это хорошо!

Перевод статьи: Taming the Cascade With BEM and Modern CSS Selectors

Была ли вам полезна эта статья?
[1 / 5]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments