Узнайте, как использовать `@scope` для выбора элементов только в пределах ограниченного поддерева вашего DOM.
Опубликовано: 4 октября 2023 г.
При написании селекторов вы можете столкнуться с дилеммой: с одной стороны, вы хотите достаточно точно указать, какие элементы вы выбираете. С другой стороны, вы хотите, чтобы ваши селекторы оставались легко переопределяемыми и не были жестко привязаны к структуре DOM.
Например, если вы хотите выбрать «главное изображение в области контента компонента карточки» — что является довольно специфическим выбором элемента — вам, скорее всего, не понадобится писать селектор типа .card > .content > img.hero .
- Этот селектор имеет довольно высокую специфичность
(0,3,1), что затрудняет его переопределение по мере роста вашего кода. - Использование комбинатора "прямой дочерний элемент" обеспечивает тесную связь с DOM-структурой. В случае изменения разметки потребуется также изменить CSS-код.
Но также не стоит указывать в качестве селектора для этого элемента только img , поскольку это выберет все изображения на вашей странице.
Найти правильный баланс в этом вопросе зачастую бывает непросто. За прошедшие годы некоторые разработчики придумали решения и обходные пути, чтобы помочь вам в подобных ситуациях. Например:
- Такие методологии, как BEM, предписывают присваивать элементу класс
card__img card__img--hero, чтобы снизить специфичность, но при этом обеспечить возможность точного выбора элементов. - Решения на основе JavaScript, такие как Scoped CSS или Styled Components, переписывают все ваши селекторы, добавляя к ним случайно сгенерированные строки — например,
sc-596d7e0e-4— чтобы предотвратить их воздействие на элементы на другой стороне страницы. - Некоторые библиотеки даже полностью отказываются от селекторов и требуют размещать триггеры стилей непосредственно в самой разметке.
А что, если вам ничего из этого не нужно? Что, если CSS даст вам возможность довольно точно выбирать элементы, не требуя при этом написания высокоспецифичных селекторов или селекторов, тесно связанных с вашим DOM? Вот тут-то и пригодится @scope , предлагающий способ выбора элементов только внутри поддерева вашего DOM.
Представляем @scope
С помощью @scope можно ограничить область действия селекторов. Для этого устанавливается корневая область видимости , которая определяет верхнюю границу поддерева, на которое вы хотите воздействовать. При установленной корневой области видимости содержащиеся в них правила стилей — называемые правилами стилей с областью видимости — могут выбирать элементы только из этого ограниченного поддерева DOM.
Например, чтобы воздействовать только на элементы <img> в компоненте .card , необходимо установить .card в качестве корневого элемента области видимости правила @scope .
@scope (.card) {
img {
border-color: green;
}
}
Правило стиля с ограниченной областью видимости img { … } может эффективно выбирать только элементы <img> , находящиеся в области видимости соответствующего элемента .card .
Чтобы предотвратить выбор элементов <img> внутри области содержимого карточки ( .card__content ), можно сделать селектор img более специфичным. Другой способ сделать это — использовать тот факт, что правило @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 . При объявлении этого свойства внутри области видимости `donut` color по-прежнему наследует дочерние элементы внутри отверстия `donut`.
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
В приведенном примере элемент .card__content и его дочерние элементы имеют hotpink цвет, поскольку они наследуют значение от .card .