Ограничение области действия селекторов с помощью CSS правила @scope

Источник: «Limit the reach of your selectors with the CSS @scope at-rule»
Узнайте, как использовать @scope для выбора элементов только в ограниченном поддереве DOM.

Тонкое искусство написания CSS селекторов

При написании селекторов вы можете оказаться разрываемым между двумя мирами. С одной стороны, вы хотите быть достаточно конкретным в выборе элементов. С другой стороны, вы хотите, чтобы селекторы легко переопределялись и не были жёстко привязаны к структуре DOM.

Например, если вы хотите выбрать изображение hero в области содержимого компонента card, что является довольно специфическим выбором элемента, вы, скорее всего, не захотите писать селектор типа .card > .content > img.hero.

Но также не стоит писать просто img в качестве селектора для этого элемента, так как в этом случае будут выбраны все элементы изображения на странице.

Зачастую найти правильный баланс в этом вопросе довольно сложно. За прошедшие годы некоторые разработчики придумали решения и обходные пути, помогающие в подобных ситуациях. Например:

Но что, если вам не нужно ничего из этого? Что, если бы CSS давал возможность достаточно точно определять, какие элементы вы выбираете, не требуя при этом написания селекторов высокой специфичности или жёсткой привязки к DOM? Вот тут-то и приходит на помощь @scope, предлагающий способ выбора элементов только в пределах поддерева DOM.

Представляем @scope

С помощью @scope можно ограничить область действия селекторов. Для этого задаётся корень охвата/области действия (scoping root), определяющий верхнюю границу поддерева, на которое вы хотите ориентироваться. Если задан корень охвата, то содержащиеся в нем правила стилей, называемые правилами стилей с ограничением, могут выбирать только из этого ограниченного поддерева DOM.

Например, чтобы выбрать только элементы <img> в компоненте .card, необходимо задать .card в качестве корня области действия в at-правиле @scope.

@scope (.card) {
img {
border-color: green;
}
}

Ограниченное областью действия правило стиля img { ... } может эффективно выбирать только те элементы <img>, которые находятся в области действия сопоставленного элемента .card.

See the Pen

Чтобы не выбирать элементы <img> внутри области контента карточки (.card__content), можно сделать селектор img более конкретным. Другой способ сделать это — использовать тот факт, что at-правило @scope также принимает ограничение на область действия, определяющее нижнюю границу.

@scope (.card) to (.card__content) {
img {
border-color: green;
}
}

Это ограниченное областью действия правило стиля нацелено только на элементы <img>, расположенные между элементами .card и .card__content в дереве предков. Такой тип области действия — с верхней и нижней границей — часто называют область действия "пончика" (donut scope)

See the Pen

Селектор :scope

По умолчанию все правила стилей относятся к корню диапазона. Можно также нацелиться на сам корневой элемент. Для этого используется селектор :scope.

@scope (.card) {
:scope {
/* Выбирает саму совпадающую .card */
}
img {
/* Выбираются элементы img, являющиеся дочерними по отношению к .card */
}
}

Селекторы, находящиеся внутри правил стилей, неявно получают добавление :scope. Если вы хотите, вы можете явно указать на это, добавив :scope самостоятельно. В качестве альтернативы можно добавить к селектору префикс &, из раздела Вложенность CSS.

@scope (.card) {
img {
/* Выбираются элементы img, являющиеся дочерними по отношению к .card */
}
:scope img {
/* Также выбираются элементы img, являющиеся дочерними по отношению к .card */
}
& img {
/* Также выбираются элементы img, являющиеся дочерними по отношению к .card */
}
}

Ограничение области действия может использовать псевдокласс :scope, требующий определённого отношения к корню области действия:

/* .content является ограничением только тогда, когда он является непосредственным дочерним элементом :scope */
@scope (.media-object) to (:scope > .content) { ... }

Ограничение области действия может также ссылаться на элементы за пределами корня области действия с помощью :scope. Например:

/* .content является ограничением только в том случае, если :scope находится внутри .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Обратите внимание, что сами по себе правила стиля не могут выходить за пределы поддерева. Выделения типа :scope + p недействительны, поскольку они пытаются выбрать элементы, не находящиеся в области действия.

@scope и специфичность

Селекторы, используемые в прелюдии для @scope, не влияют на специфичность содержащихся в ней селекторов. В приведённом ниже примере специфичность селектора img по-прежнему равна (0,0,1).

@scope (#sidebar) {
img { /* Специфичность = (0,0,1) */

}
}

Специфичность :scope — это специфичность обычного псевдокласса, а именно (0,1,0).

@scope (#sidebar) {
:scope img { /* Специфичность = (0,1,0) + (0,0,1) = (0,1,1) */

}
}

Поскольку & десуггерируется с помощью :is(), специфичность & вычисляется по правилам специфичности :is(): специфичность & равна специфичности его наиболее специфичного аргумента.

Применительно к данному примеру специфичность :is(#sidebar, .card) равна специфичности его наиболее специфичного аргумента, а именно #sidebar, и поэтому становится равной (1,0,0). Объединив это со специфичностью img, которая равна (0,0,1), вы получите (1,0,1) в качестве специфичности для всего сложного селектора.

@scope (#sidebar, .card) {
& img { /* Специфичность = (1,0,0) + (0,0,1) = (1,0,1) */

}
}

Различие между :scope и & внутри @scope

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

Благодаря этому можно использовать & несколько раз. В отличие от :scope, который можно использовать только один раз, поскольку нельзя сопоставить корень охвата внутри корня охвата.

@scope (.card) {
& & { /* Выбирает `.card` в соответствующем корне .card */
}
:root :root { /* ❌ Не работает */

}
}

Область действия без прелюдий

При написании встроенных стилей с помощью элемента <style> можно распространить правила стиля на объемлющий родительский элемент элемента <style>, не указывая корень охвата. Для этого необходимо опустить прелюдию @scope.

<div class="card">
<div class="card__header">
<style>
@scope {
img {
border-color: green;
}
}
</style>
<h1>Card Title</h1>
<img src="" height="32" class="hero">
</div>
<div class="card__content">
<p><img src="" height="32"></p>
</div>
</div>

В приведённом примере правила определения области действия нацелены только на элементы внутри div с именем класса card__header, поскольку этот div является родительским элементом элемента <style>.

See the Pen

@scope в каскаде

Внутри CSS-каскада @scope также добавляет новый критерий: близость к области действия (scope proximity). Этот шаг идёт после специфичности, но перед порядком появления.

Визуализация CSS-каскада.
Визуализация CSS-каскада.

В соответствии со спецификацией:

При сравнении деклараций, появляющихся в стилевых правилах с разными корнями охвата, побеждает декларация, имеющая наименьшее количество родовых или дочерних элементов между корнем охвата и объектом стилевого правила.

Этот новый шаг удобен при вложении нескольких вариантов компонента. Приведём пример, в котором пока не используется @scope:

<style>
.light { background: #ccc; }
.dark { background: #333; }
.light a { color: black; }
.dark a { color: white; }
</style>
<div class="light">
<p><a href="#">What color am I?</a></p>
<div class="dark">
<p><a href="#">What about me?</a></p>
<div class="light">
<p><a href="#">Am I the same as the first?</a></p>
</div>
</div>
</div>

При просмотре этого небольшого фрагмента разметки третья ссылка будет белой, а не чёрной, несмотря на то, что она является дочерней по отношению к div, к которому применён класс .light. Это связано с критерием порядка появления, который каскад использует для определения победителя. Он видит, что .dark a был объявлен последним, поэтому он выиграет у правила .light a

See the Pen

Теперь эта проблема решена с помощью критерия близости к области действия:

@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}

@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}

Поскольку оба селектора a имеют одинаковую специфику, в действие вступает критерий близости к области действия. Он оценивает оба селектора по близости к их корню. Для третьего элемента a до корня области действия .light всего один прыжок, а до .dark — два. Поэтому селектор a в .light победит.

See the Pen

Заключительное примечание: селекторная изоляция, а не стилевая изоляция

Важно отметить, что @scope ограничивает область действия селекторов, но не обеспечивает стилевую изоляцию. Свойства, наследуемые дочерними элементами, будут наследоваться и за пределами нижней границы @scope. Одним из таких свойств является свойство color. Если объявить его внутри области действия пончика, цвет все равно будет наследоваться дочерним элементам внутри отверстия пончика.

@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}

See the Pen

В приведённом примере элемент .card__content и его дочерние элементы имеют цвет hotpink, поскольку наследуют значение от .card.

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

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

TypeScript: Сравнение Типа и Интерфейса

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

Заполнение пропусков в результатах статистических временных рядов