Как CSS @scope может заменить БЭМ

Источник: «How CSS @scope can replace BEM»
Одним из наиболее распространённых и сложных вопросов, с которыми сталкиваются фронтенд-инженеры, являются соглашения об именовании CSS. С популярностью метода Блок Элемент Модификатор (БЭМ) многие привыкли организовывать свои стили по удобной схеме.

Грядущая реализация @scope в Chrome может ещё больше расширить преимущества БЭМ, позволив определять стили на уровне блоков в таблице стилей. Это может упростить поддержку стилей и обеспечить более жёсткий контроль над CSS-каскадом, влияющим на любое фронтенд-приложение.

В этой статье мы покажем, как использовать функцию @scope в Chrome и как с её помощью заменить БЭМ во фронтенд-проектах. Мы рассмотрим несколько примеров, все из которых вы можете проверить на примерах из проекта на GitHub.

Что такое CSS @scope

В готовящемся к выпуску Chrome 118 функция @scope позволяет создавать диапазоны CSS стилей на уровне блоков. Это даёт разработчикам больше возможностей для управления CSS стилями, поскольку теперь мы можем специально определять область видимости для разделов представления непосредственно в CSS файлах.

Рассмотрим следующий пример HTML:

<main className="sample-page">
<h1>With Scope</h1>
<section className="first-section">
<p>some text</p>
<p>
some text and then a <a href="/">back link</a>
</p>
</section>
<section className="second-section">
<h2>Dog Picture</h2>
<div>
<p>second section paragraph text</p>
</div>
<img src={'./DOG_1.jpg'} alt="dog" />
</section>
</main>

В этом HTML мы можем стилизовать элементы в области стилей second-section следующим образом:

.second-section {
display: flex;
flex-direction: column;
border: solid;
padding: 40px;
margin: 20px;
}
@scope (.second-section) {
h2 {
text-align: center;
}
img {
max-width: 400px;
max-height: 100%;
}
div {
display: flex;
justify-content: center;
margin: 20px;
}
p {
max-width: 200px;
text-align: center;
background-color: pink;
color: forestgreen;
padding: 10px;
border-radius: 20px;
font-size: 24px;
}
}

С помощью @scope можно также создать область видимости "пончик" ("donut" scope), в которой начальная и конечная секции определяются для набора стилей и элементов в них. В том же HTML, приведённом выше, область видимости "пончик" может определять стили от начальной области sample-page до места, где находится стилизованная область second-section:

/* donut scope */
@scope (.sample-page) to (.second-section) {
p {
font-size: 24px;
background-color: forestgreen;
color: pink;
text-align: center;
padding: 10px;
}
a {
color: red;
font-size: 28px;
text-transform: uppercase;
}
}

Самое замечательное в этом то, что это функционирует очень похоже на то, что можно сделать с помощью стилизации БЭМ — но с меньшим количеством кода.

Поддержка браузерами CSS @scope

По состоянию на 2 октября 2023 года CSS @scope ещё не выпущен, поэтому для его использования необходимо включить флаг экспериментальных веб-функций. Для этого сначала откройте вкладку в Chrome и перейдите по адресу chrome://flags/, затем найдите и включите флаг Experimental Web Platform features:

Флаг экспериментальных функций Chrome включён
Флаг экспериментальных функций Chrome включён

После установки экспериментального флага в Chrome добавление @scope к таблицам стилей должно работать в любой сессии Chrome.

Что такое БЭМ

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

Рассмотрим большую HTML-страницу, содержащую множество элементов с различными стилями. После задания нескольких начальных имён стилей может оказаться сложным поддерживать их при масштабировании страницы. БЭМ пытается решить эту проблему, структурируя имена стилей в соответствии с тем, что на самом деле является стилем.

Блок — это содержащий элемент HTML. Рассмотрим примерно такой HTML:

<main className="sample-page">
<h1 className="sample-page__title">With BEM</h1>
<section className="sample-page__first-section">
<p className="sample-page__first-section--first_line">
some text
</p>
<p className="sample-page__first-section--second-line">
some text and then a{' '}
<a
className="sample-page__first-section--second-line-link"
href="/"
>

back link
</a>
</p>
</section>
</main>

В этом HTML фрагменте:

БЭМ хорошо масштабируется, особенно если вы используете SASS для обёртывания стилей в группы с оператором &, чтобы создать нечто подобное:

.sample-page {
display: flex;
flex-direction: column;
margin-top: 10px;

&__title {
font-size: 48px;
color: forestgreen;
}

&__first-section {
font-size: 24px;
border: solid;
padding: 40px;
margin: 20px;

&--first-line{
font-size: 24px;
background-color: forestgreen;
color: pink;
text-align: center;
padding: 10px;
}
}
}

Проблема заключается в том, что в очень большом проекте это приводит к появлению очень больших CSS- или SASS-файлов, управлять которыми в рамках масштаба может быть затруднительно. Можно заменить БЭМ стили на @scope и сделать определения стилей меньше и более управляемыми. Мы продемонстрируем, как это работает, в следующем разделе.

Рефакторинг БЭМ для использования @scope

Лучше всего преимущества использования @scope можно продемонстрировать на примере приложения с одним из ведущих фреймворков или библиотек, например React. В примере приложения на GitHub в папке react-example есть проект, в котором страница сначала стилизована с помощью БЭМ, а затем подвергнута рефакторингу с использованием @scope.

Вы можете запустить приложение и нажать на кнопки WithBEM или WithScope, чтобы увидеть конкретные реализации. Компоненты и таблицы стилей имеют соответствующие имена с префиксами WithBEM или WithScope в папках pages и styles соответственно.

Начиная с компонента WithBEMPage.tsx, стилизованного под БЭМ, мы впервые видим HTML, стилизованный в рамках метода БЭМ:

<main className="sample-page">
<h1 className="sample-page__title">With BEM</h1>
<section className="sample-page__first-section">
<p className="sample-page__first-section--first_line">
some text
</p>
<p className="sample-page__first-section--second-line">
some text and then a{' '}
<a
className="sample-page__first-section--second-line-link"
href="/"
>

back link
</a>
</p>
</section>
<section className="sample-page__second-section">
<h2 className="sample-page__second-section--title">
Dog Picture
</h2>
<div className="sample-page__second-section--div">
<p className="sample-page__second-section--div-paragraph">
second section paragraph text
</p>
</div>
<img
className="sample-page__second-section--image"
src={'./DOG_1.jpg'}
alt="dog"
/>
</section>
</main>

В компоненте WithScopePage.tsx мы видим, насколько чистым получился рефакторинг:

<main className="sample-page">
<h1>With Scope</h1>
<section className="first-section">
<p>some text</p>
<p>
some text and then a <a href="/">back link</a>
</p>
</section>
<section className="second-section">
<h2>Dog Picture</h2>
<div>
<p>second section paragraph text</p>
</div>
<img src={'./DOG_1.jpg'} alt="dog" />
</section>
</main>

Для рефакторинга из БЭМ в @scope достаточно найти группы стилей, а затем соответствующим образом добавить в них свои стили. Сначала рассмотрим секцию заголовка. В исходном файле WithBEMPage.tsx для каждой секции были определены свои стили. В версии @scope имеется более лаконичное определение стилей для конкретных элементов:

.sample-page {
display: flex;
flex-direction: column;
margin-top: 10px;
}
/* заменён */
/* .sample-page__title {
font-size: 48px;
color: forestgreen;
} */

/* donut scope */
@scope (.sample-page) to (.first-section) {
h1 {
font-size: 48px;
color: forestgreen;
}
}

Аналогично, в первой секции контента исходные стили БЭМ выглядят следующим образом:

.sample-page__first-section {
font-size: 24px;
border: solid;
padding: 40px;
margin: 20px;
}
.sample-page__first-section--first_line {
font-size: 24px;
background-color: forestgreen;
color: pink;
text-align: center;
padding: 10px;
}
.sample-page__first-section--second-line {
font-size: 24px;
background-color: forestgreen;
color: pink;
text-align: center;
padding: 10px;
}
.sample-page__first-section--second-line-link {
color: red;
font-size: 28px;
text-transform: uppercase;
}

Рефакторинг этой первой секции с помощью @scope позволил получить более лаконичное определение стиля:

.first-section {
font-size: 24px;
border: solid;
padding: 40px;
margin: 20px;
}
/* donut scope */
@scope (.sample-page) to (.second-section) {
p {
font-size: 24px;
background-color: forestgreen;
color: pink;
text-align: center;
padding: 10px;
}
a {
color: red;
font-size: 28px;
text-transform: uppercase;
}
}

Другим приятным побочным эффектом этого стало то, что вид HTML стал меньше и легче читается. Если рассматривать предыдущий вариант:

<section className="sample-page__first-section">
<p className="sample-page__first-section--first_line">
some text
</p>
<p className="sample-page__first-section--second-line">
some text and then a{' '}
<a
className="sample-page__first-section--second-line-link"
href="/"
>

back link
</a>
</p>
</section>

А потом:

<section className="first-section">
<p>some text</p>
<p>
some text and then a <a href="/">back link</a>
</p>
</section>

Пройдя по двум примерам компонентов, можно применить рефакторинг к каждой секции. В итоге можно отметить, что стилизация стала чище и легче для восприятия.

Дополнительные преимущества @scope перед БЭМ

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

При работе с любым фронтенд-проектом разработчикам приходится учитывать побочные эффекты от каскада, когда стили приводят к странным результатам. Используя @scope, можно потенциально контролировать побочные эффекты каскада за счёт жёсткой привязки к элементам.

В примере GitHub проекта в папке html-css находятся два базовых HTML-файла, содержащие пример страницы, на которую влияет каскад. Эти примеры были модифицированы на основе примеров, приведённых в статье Bram.us A Quick Introduction to CSS Scope.

В файле no_scope.html стили и несколько элементов определены следующим образом:

<!DOCTYPE html>
<html>
<head>
<title>Plain HTML</title>
<style>
.light {
background: #ccc;
}
.dark {
background: #333;
}
.light a {
color: red;
}
.dark a {
color: yellow;
}
div {
padding: 2rem;
}
div > div {
margin: 0 0 0 2rem;
}
p {
margin: 0 0 2rem 0;
}
</style>
</head>
<body>
<div class="light">
<p><a href="#">First Level</a></p>
<div class="dark">
<p><a href="#">Second Level</a></p>
<div class="light">
<p><a href="#">Third Level</a></p>
</div>
</div>
</div>
</body>
</html>

В результате получается следующее:

Демонстрация CSS @scope
Демонстрация CSS @scope

Проблема заключается в том, что в соответствии с заданным CSS Third Level должен быть выделен красным цветом, а не жёлтым. Это побочный эффект CSS-каскада, поскольку стилизация страницы интерпретируется на основе порядка появления, и поэтому Third Level воспринимается как жёлтый, а не красный. Взяв диаграмму из оригинального сообщения Bram.us, можно увидеть порядок, в котором каскад CSS оценивает селекторы и стили:

Визуализация каскада с введённой и выделенной близостью @scope. Источник: bram.us
Визуализация каскада с введённой и выделенной близостью @scope. Источник: [bram.us](https://bram.us/)

Без использования @scope CSS каскад переходит от Специфичности непосредственно к Порядку Появления. При использовании @scope CSS каскад в первую очередь учитывает элементы @scope. Это можно увидеть, добавив @scope специально для стилей .light и .darkв примере.

Во-первых, измените исходные HTML и CSS следующим образом:

<!DOCTYPE html>
<html>
<head>
<title>Plain HTML</title>
<style>
.light {
background: #ccc;
}
.dark {
background: #333;
}
div {
padding: 2rem;
}
div > div {
margin: 0 0 0 2rem;
}
p {
margin: 0 0 2rem 0;
}
@scope (.light) {
:scope {
background: white;
}
a {
color: red;
}
}
@scope (.dark) {
:scope {
background: black;
}
a {
color: yellow;
}
}
</style>
</head>
<body>
<div class="light">
<p><a href="#">First Level</a></p>
<div class="dark">
<p><a href="#">Second Level</a></p>
<div class="light">
<p><a href="#">Third Level</a></p>
</div>
</div>
</div>
</body>
</html>

В результате мы получаем вывод, который выглядит следующим образом:

Окончательный результат @scope
Окончательный результат @scope

Заключение

В этой статье мы рассмотрели способы рефакторинга приложений в БЭМ стиле для использования новой функции @scope, появившейся в Chrome. Мы рассмотрели принцип работы @scope, а затем переработали простую страницу из БЭМ в @scope.

Новая функция @scope потенциально может стать большим выигрышем для разработчиков фронтенда. Однако её поддержка должна быть реализована и в других браузерах, что может занять некоторое время. Пока же это, безусловно, интересная функция, которая может оказаться очень полезной для стилизации фронтенд-проектов.

Похожие статьи

Дополнительные материалы

Предыдущая Статья

Новое в Symfony 6.4: Упрощённый выход из системы

Следующая Статья

Оптимизация Laravel Eloquent запросов