CSS @scope 规则
本文是 MDN | @scope 的翻译,并根据我自己的理解进行了适当地修改和补充。采取与原文相同的许可证。
截至 2024 年 5 月,Firefox 任然不支持 @scope
规则,但是 Chrome 与 Safari 已经支持。详情查看 Can i use @scope。
@scope
可以让我们精确的定位 DOM 子树中的元素,而无需编写难以覆盖的过于特定的选择器,并且不会将选择器与 DOM 结构耦合得太紧密。
在 JavaScript 里,可以使用 CSSScopeRule
访问 @scope
规则。
语法
@scope
包含一个或多个规则集(称为作用域样式规则),并定义将它们应用于选定元素的范围。 @scope
可以通过两种方式使用:
-
像其他 at 规则一样,作为 CSS 中的独立块,可以指定规则的作用域上限和下限。
@scope (scope root) to (scope limit) {
rulesets
} -
作为内联样式(
<style>
标签内的样式)包含在 HTML 中的,在这种情况下,两个限定参数可以被省略,并且包含的规则集自动将范围限定为<style>
元素的封闭父元素。<parent-element>
<style>
@scope {
rulesets
}
</style>
</parent-element>
描述
复杂的 Web 文档可能包括页眉、页脚、新闻文章、地图、媒体播放器、广告等组件。随着复杂性的增加,有效管理这些组件的样式变得更加困难,而通过限定样式的范围有助于我们管理这种复杂性。让我们考虑以下 DOM 树:
body
└─ article.feature
├─ section.article-hero
│ ├─ h2
│ └─ img
│
├─ section.article-body
│ ├─ h3
│ ├─ p
│ ├─ img
│ ├─ p
│ └─ figure
│ ├─ img
│ └─ figcaption
│
└─ footer
├─ p
└─ img
如果想选择带有 article-body
类的 <section>
内的 <img>
元素,可以执行以下操作:
-
编写一个选择器,例如
.feature > .article-body > img
。然而,它具有很高的特异性,因此很难被覆盖,并且与 DOM 结构紧密耦合。如果 DOM 结构发生变化,可能需要重写 CSS。 -
写一些不太具体的内容,例如
.article-body img
。但是,这将选择该部分内的所有图像。我们的目的是选择<section>
的直接子级<img>
,但是figure
内的img
也符合这个 CSS 选择器。
这就是 @scope
有用的地方。它可以定义一个精确的范围,让 CSS 选择器只作用于这个范围之内。例如,上述问题可以这么解决:
@scope (.article-body) to (figure) {
img {
border: 5px solid black;
background-color: goldenrod;
}
}
由于 .article-body
指定了 CSS 规则生效的上限,而 <figure>
指定了下限。CSS 规则只作用于 .article-body
与 <figure>
之间的元素,不会选择 <figure>
元素内的元素。
这种具有上限和下限的范围通常称为环形范围。
如果你想选择带有 article-body
类的 <section>
内的所有图像,也可以省略范围限制:
@scope (.article-body) {
img {
border: 5px solid black;
background-color: goldenrod;
}
}
或者,你可以采用刚刚提到的第二种写法,将 @scope
块内联包含在 <style>
元素内,该元素又位于 <section>
内:
<section class="article-body">
<style>
@scope {
img {
border: 5px solid black;
background-color: goldenrod;
}
}
</style>
<!-- ... -->
</section>
需要注意的是,虽然 @scope
将选择器的应用隔离到特定的 DOM 子树,但它并不能 完全隔离这些子树内应用的样式。这在继承中最为明显——子级继承的属性(例如颜色或字体系列)仍然会被继承,超出任何设置的范围限制。
:scope
伪类
在 @scope
块的上下文中,:scope
伪类代表作用域根,它提供了一种从作用域内部将样式应用到作用域根本身的简单方法:
@scope (.feature) {
:scope {
background: rebeccapurple;
color: antiquewhite;
font-family: sans-serif;
}
}
事实上,会被 :scope
隐式地添加到所有作用域样内的式规则的前面。也就是说,任何 @scope
作用域内的 CSS 选择器前面都会被浏览器自动加上 :scope
,但是不计入 CSS 特异性。
以下的三条规则是等价 的 (但特异性不相等,查看@scope
的特异性):
@scope (.feature) {
img { ... }
:scope img { ... }
& img { ... }
}
注意事项
-
范围限制可以使用
:scope
来指定范围限制和根之间的特定关系要求。例如:/* 指定下限为 .article-body 的直接 figure 子级 */
@scope (.article-body) to (:scope > figure) { ... } -
范围限制可以使用
:scope
引用范围根之外的元素,也就是说上下限都是单纯的 CSS 选择器,并不要求下限必须从上限开始选择。例如:@scope (.article-body) to (.feature :scope figure) { ... }
-
下限必须是上限的子孙元素(原文是:Scoped style rules can't escape the subtree)。像
:scope + p
这样的选择是无效的,因为该选择将位于上限的子树之外。 -
选择器的上限可以指定多个,在这种情况下将视为定义多个范围。在以下示例中,样式将应用于
<section>
内具有article-hero
或article-body
类的任何<img>
,而如果<img>
嵌套在<figure>
内,则不会被选中:@scope (.article-hero, .article-body) to (figure) {
img {
border: 5px solid black;
background-color: goldenrod;
}
}
@scope
的CSS特异性
简单来说, @scope
不会改变其内部规则的特异性,例如:
@scope (.article-body) {
/* img 的特异性是 0-0-1 */
img { ... }
}
但是,如果显式地将 :scope
伪类添加到作用域选择器之前,则在计算其特异性时需要将其考虑在内。:scope
与所有常规伪类一样,具有 0-1-0 的特异性。例如:
@scope (.article-body) {
/* :scope img 的特异性为 0-1-0 + 0-0-1 = 0-1-1 */
:scope img { ... }
}
当在 @scope
块内使用 &
选择器时,&
代表作用域根选择器(上限选择器);它在内部计算为包装在 :is()
伪类函数内的选择器。可能听不懂,直接看例子就明白了:
@scope (figure, #primary) {
& img { ... }
}
& img
等价于 :is(figure, #primary) img
。:is()
选择器的特异性为它内部生效的最大的 CSS 选择器的特异性,在这个例子中是 #primary
,特异性为 1-0-0
,因此这个条规则的特异性为 1-0-1
。
:scope
与 &
的不同之处
:scope
表示匹配的作用域根(指定的是元素本身),而 &
表示用于匹配作用域根的选择器(是选择器而不是选中的元素)。因此,可以多次应用 &
(选择器可以重复使用多次)。但是,只能使用 :scope
一次(作用域根不能作为自身的子元素)。
@scope (.feature) {
/* .feature 内的 .feature */
& & { ... }
/* 错误 */
:scope :scope { ... }
}
解决 @scope
冲突
@scope
为 CSS 添加了一个新标准:范围邻近度。这表明当两个作用域具有冲突的样式时,将应用 DOM 树层次结构中到作用域根的跳跃次数最少的样式。让我们看一个例子来看看这意味着什么。
采用以下 HTML 片段,其中不同主题的卡片相互嵌套:
<div class="light-theme">
<p>Light theme text</p>
<div class="dark-theme">
<p>Dark theme text</p>
<div class="light-theme">
<p>Light theme text</p>
</div>
</div>
</div>
下面的 CSS 很符合直觉,但可惜是错误的:
.light-theme {
background: #ccc;
}
.dark-theme {
background: #333;
}
.light-theme p {
color: black;
}
.dark-theme p {
color: white;
}
最里面的段落的文字应该被设置为黑色,因为它位于浅色主题卡内。但是,它是 .light-theme p
和 .dark-theme p
选择器的目标。由于 .dark-theme p
规则在源代码中出现得较晚,因此应用了该规则,因而该段落最终被错误地设置为白色。
可以使用 @scope
解决此问题,如下所示:
@scope (.light-theme) {
:scope {
background: #ccc;
}
p {
color: black;
}
}
@scope (.dark-theme) {
:scope {
background: #333;
}
p {
color: white;
}
}
现在,最里面的段落已正确着色为黑色。这是因为它距离 .light-theme
作用域根仅一级 DOM 树层次结构,但距离 .dark-theme
作用域根两级。因此,.light-theme
被应用。
范围邻近性原则会覆盖源代码顺序原则,但其本身会被其他更高优先级的样式(例如 !importance
、@layers
和特异性)所覆盖。
例子
@scope
内的基本样式
在此示例中,我们使用两个单独的 @scope
块分别将元素内的链接与 .light-scheme
和 .dark-scheme
类进行匹配。请注意 :scope
如何用于选择作用域根本身并为其提供样式。在此示例中,范围根是应用了类的 <div>
元素。
指定作用域根和范围限制
在此示例中,我们有一个匹配 DOM 结构的 HTML 片段。该结构代表了一个典型的文章摘要。需要注意的关键特性是 <img>
元素,它嵌套在结构的各个级别中。
这个示例的目的是展示如何使用作用域根和限制来样式化从层次结构的顶部开始的 <img>
元素,但仅限于(不包括)<figure>
元素内的 <img>
—— 实际上创建了一个环形范围。