Узнайте, как использовать @scope для выбора элементов только в пределах ограниченного поддерева вашего DOM.
Тонкое искусство написания селекторов CSS
При написании селекторов вы можете оказаться разрывающимся между двумя мирами. С одной стороны, вам нужно быть очень конкретным в отношении того, какие элементы вы выбираете. С другой стороны, вы хотите, чтобы ваши селекторы легко переопределялись и не были тесно связаны со структурой DOM.
Например, если вы хотите выбрать «главное изображение в области содержимого компонента карты» — что является довольно специфическим выбором элемента — вы, скорее всего, не захотите писать селектор типа .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 вы можете ограничить охват ваших селекторов. Вы делаете это, устанавливая корень области видимости , который определяет верхнюю границу поддерева, на которое вы хотите ориентироваться. При наличии корневого набора области действия содержащиеся в нем правила стиля (называемые правилами стиля с ограниченной областью действия ) могут выбирать только из этого ограниченного поддерева 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 в дереве предков. Этот тип области видимости — с верхней и нижней границей — часто называют кольцевой областью действия.
Селектор :scope
По умолчанию все правила стиля с ограниченной областью действия относятся к корневой области области действия. Также возможно настроить таргетинг на сам корневой элемент области видимости. Для этого используйте селектор :scope .
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
Селекторы внутри правил стиля с ограниченной областью действия неявно добавляют к началу :scope . Если хотите, вы можете указать это явно, добавив :scope самостоятельно. В качестве альтернативы вы можете добавить селектор & из CSS Nesting .
@scope (.card) {
img {
/* Selects img elements that are a child of .card */
}
:scope img {
/* Also selects img elements that are a child of .card */
}
& img {
/* Also selects img elements that are a child of .card */
}
}
Ограничение области видимости может использовать псевдокласс :scope чтобы требовать определенного отношения к корню области видимости:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
Ограничение области действия также может ссылаться на элементы за пределами их корня области действия, используя :scope . Например:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
Обратите внимание, что сами правила стиля с ограниченной областью действия не могут выйти за пределы поддерева. Такие выборы, как :scope + p , недействительны, поскольку при этом пытаются выбрать элементы, не входящие в область видимости.
@scope и специфика
Селекторы, которые вы используете в прелюдии к @scope , не влияют на специфичность содержащихся в них селекторов. В приведенном ниже примере специфичность селектора img по-прежнему равна (0,0,1) .
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
…
}
}
Специфика :scope заключается в том, что он является обычным псевдоклассом, а именно (0,1,0) .
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
…
}
}
В следующем примере внутренне & перезаписывается на селектор, который используется для корня области видимости, завернутый в селектор :is() . В конце концов, браузер будет использовать :is(#sidebar, .card) img в качестве селектора для сопоставления. Этот процесс известен как обессахаривание .
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
…
}
}
Поскольку & очищается с помощью :is() , специфичность & рассчитывается в соответствии с правилами специфичности :is() : специфичность & равна специфичности его наиболее конкретного аргумента.
Применительно к этому примеру, особенностью :is(#sidebar, .card) является специфика его самого конкретного аргумента, а именно #sidebar , и поэтому он становится (1,0,0) . Объедините это со спецификой img (0,0,1) — и вы получите (1,0,1) как специфику для всего сложного селектора.
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
…
}
}
Разница между :scope и & внутри @scope
Помимо различий в том, как рассчитывается специфичность, еще одно различие между :scope и & заключается в том, что :scope представляет совпадающий корень области видимости, тогда как & представляет селектор, используемый для сопоставления корня области видимости.
По этой причине & можно использовать несколько раз. В этом отличие от :scope , который вы можете использовать только один раз, поскольку вы не можете сопоставить корень области видимости внутри корня области видимости.
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
Объем без прелюдий
При написании встроенных стилей с помощью элемента <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 Cascade @scope также добавляет новый критерий: область видимости близости . Этот шаг следует за конкретикой, но перед порядком появления.
Согласно спецификации :
При сравнении объявлений, которые появляются в правилах стиля с разными корнями области видимости, побеждает объявление с наименьшим количеством переходов между поколениями или родственными элементами между корнем области видимости и субъектом правила стиля с областью действия.
Этот новый шаг пригодится при вложении нескольких вариантов компонента. Возьмем этот пример, который пока не использует @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>
При просмотре этого небольшого фрагмента разметки третья ссылка будет white , а не black , даже если она является дочерним элементом элемента 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 . При объявлении того, что находится внутри области пончика, color по-прежнему будет наследоваться до детей внутри отверстия пончика.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
В приведенном выше примере элемент .card__content и его дочерние элементы имеют hotpink цвет, поскольку они наследуют значение от .card .
(Фото на обложке Рустама Бурханова на Unsplash )