Ограничение области действия селекторов с помощью CSS правила @scope
Тонкое искусство написания CSS селекторов
При написании селекторов вы можете оказаться разрываемым между двумя мирами. С одной стороны, вы хотите быть достаточно конкретным в выборе элементов. С другой стороны, вы хотите, чтобы селекторы легко переопределялись и не были жёстко привязаны к структуре DOM.
Например, если вы хотите выбрать изображение hero в области содержимого компонента card
, что является довольно специфическим выбором элемента, вы, скорее всего, не захотите писать селектор типа .card > .content > img.hero
.
- Этот селектор имеет довольно высокую специфичность
(0,3,1)
, что затрудняет его переопределение по мере роста кода. - Благодаря использованию прямого дочернего комбинатора он жёстко привязан к структуре DOM. Если разметка изменится, необходимо будет изменить и CSS.
Но также не стоит писать просто img
в качестве селектора для этого элемента, так как в этом случае будут выбраны все элементы изображения на странице.
Зачастую найти правильный баланс в этом вопросе довольно сложно. За прошедшие годы некоторые разработчики придумали решения и обходные пути, помогающие в подобных ситуациях. Например:
- Такие методологии, как БЭМ, предписывают присваивать этому элементу класс
card__img card__img--hero
, чтобы снизить специфичность и в то же время обеспечить возможность конкретного выбора. - Решения на основе JavaScript, такие, как Локальный (scoped) CSS или Styled Components, переписывают все селекторы, добавляя к ним случайно сгенерированные строки, например
sc-596d7e0e-4
, чтобы они не нацеливались на элементы, расположенные на другом конце страницы. - Некоторые библиотеки вообще отказываются от селекторов и требуют размещения триггеров стилизации непосредственно в разметке.
Но что, если вам не нужно ничего из этого? Что, если бы CSS давал возможность достаточно точно определять, какие элементы вы выбираете, не требуя при этом написания селекторов высокой специфичности или жёсткой привязки к DOM? Вот тут-то и приходит на помощь @scope
, предлагающий способ выбора элементов только в пределах поддерева DOM.
Представляем @scope
С помощью @scope
можно ограничить область действия селекторов. Для этого задаётся корень охвата/области действия (scoping root), определяющий верхнюю границу поддерева, на которое вы хотите ориентироваться. Если задан корень охвата, то содержащиеся в нем правила стилей, называемые правилами стилей с ограничением, могут выбирать только из этого ограниченного поддерева DOM.
Например, чтобы выбрать только элементы <img>
в компоненте .card
, необходимо задать .card
в качестве корня области действия в at-правиле @scope
.
@scope (.card) {
img {
border-color: green;
}
}
Ограниченное областью действия правило стиля img { ... }
может эффективно выбирать только те элементы <img>
, которые находятся в области действия сопоставленного элемента .card
.
Чтобы не выбирать элементы <img>
внутри области контента карточки (.card__content
), можно сделать селектор img
более конкретным. Другой способ сделать это — использовать тот факт, что at-правило @scope
также принимает ограничение на область действия, определяющее нижнюю границу.
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
Это ограниченное областью действия правило стиля нацелено только на элементы <img>
, расположенные между элементами .card
и .card__content
в дереве предков. Такой тип области действия — с верхней и нижней границей — часто называют область действия "пончика" (donut scope)
Селектор :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>
.
@scope
в каскаде
Внутри CSS-каскада @scope
также добавляет новый критерий: близость к области действия (scope proximity). Этот шаг идёт после специфичности, но перед порядком появления.
В соответствии со спецификацией:
При сравнении деклараций, появляющихся в стилевых правилах с разными корнями охвата, побеждает декларация, имеющая наименьшее количество родовых или дочерних элементов между корнем охвата и объектом стилевого правила.
Этот новый шаг удобен при вложении нескольких вариантов компонента. Приведём пример, в котором пока не используется @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
Теперь эта проблема решена с помощью критерия близости к области действия:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
Поскольку оба селектора a
имеют одинаковую специфику, в действие вступает критерий близости к области действия. Он оценивает оба селектора по близости к их корню. Для третьего элемента a
до корня области действия .light
всего один прыжок, а до .dark
— два. Поэтому селектор a
в .light
победит.
Заключительное примечание: селекторная изоляция, а не стилевая изоляция
Важно отметить, что @scope
ограничивает область действия селекторов, но не обеспечивает стилевую изоляцию. Свойства, наследуемые дочерними элементами, будут наследоваться и за пределами нижней границы @scope
. Одним из таких свойств является свойство color
. Если объявить его внутри области действия пончика, цвет все равно будет наследоваться дочерним элементам внутри отверстия пончика.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
В приведённом примере элемент .card__content
и его дочерние элементы имеют цвет hotpink
, поскольку наследуют значение от .card
.