Узнайте, как использовать @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 )