使用 CSS @scope at-rule 限制选择器的覆盖面

了解如何使用 @scope 仅选择 DOM 的有限子树中的元素。

浏览器支持

  • Chrome:118.
  • Edge:118。
  • Firefox:需要切换标志才能使用。
  • Safari:17.4。

来源

编写 CSS 选择器的精妙艺术

在编写选择器时,您可能会发现自己陷入两难境地。一方面,您需要明确选择哪些元素。另一方面,您希望选择器保持易于替换,并且不与 DOM 结构紧密耦合。

例如,如果您要选择“卡片组件内容区域中的主打图片”(这是一个非常具体的元素选择),您可能不想编写 .card > .content > img.hero 这样的选择器。

  • 此选择器的 (0,3,1) 特异性非常高,这会导致随着代码的增多而难以替换。
  • 由于依赖于直接子组合符,因此它与 DOM 结构紧密耦合。如果标记发生更改,您也需要更改 CSS。

不过,您也不想仅将 img 作为该元素的选择器,因为这样会选择网页中的所有图片元素。

在这种情况下,找到恰到好处的平衡点往往是一项极具挑战性的工作。多年来,一些开发者提出了一些解决方案和权宜解决方法,可帮助您应对此类情况。例如:

  • BEM 等方法规定,您应为该元素指定类 card__img card__img--hero,以保持较低的专一性,同时让您能够精确选择所需内容。
  • 基于 JavaScript 的解决方案(例如作用域 CSS样式化组件)会向选择器添加随机生成的字符串(例如 sc-596d7e0e-4),以重写所有选择器,从而防止选择器定位到网页另一侧的元素。
  • 有些库甚至完全废弃了选择器,而要求您将样式触发器直接放入标记本身。

但是,如果您不需要任何这些功能,该怎么办?如果 CSS 提供了一种方法,让您能够非常具体地选择要选择的元素,而无需编写高特异性的选择器或与 DOM 紧密耦合的选择器,那该多好!这时,@scope 就派上用场了,它可以让您仅选择 DOM 的子树中的元素。

推出 @scope

借助 @scope,您可以限制选择器的覆盖范围。为此,您可以设置限定根,该根决定了要定位的子树的上限。设置了作用域根后,包含的样式规则(称为作用域样式规则)只能从 DOM 的该受限子树中进行选择。

例如,如需仅定位到 .card 组件中的 <img> 元素,您可以将 .card 设置为 @scope at-rule 的限定根。

@scope (.card) {
    img {
        border-color: green;
    }
}

作用域限定的样式规则 img { … } 实际上只能选择位于匹配的 .card 元素的作用域内<img> 元素。

如需防止选择卡片内容区域 (.card__content) 内的 <img> 元素,您可以使 img 选择器更具体。实现此目的的另一种方法是利用 @scope at-rule 还接受用于确定下限的作用域限制这一事实。

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

此作用域限定的样式规则仅定位到祖先树中位于 .card.card__content 元素之间的 <img> 元素。这种具有上限和下限的范围限定通常称为甜甜圈范围

:scope 选择器

默认情况下,所有作用域样式规则都相对于作用域根。您也可以定位到限定范围的根元素本身。为此,请使用 :scope 选择器。

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

作用域样式规则中的选择器会隐式地添加 :scope。如果需要,您可以自行在前面附加 :scope,以明确表示该字符串。或者,您也可以在 CSS 嵌套中添加 & 选择器。

@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,因为您无法在作用域根内匹配作用域根。

@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>

在上面的示例中,限定范围的规则仅定位到类名称为 card__headerdiv 内的元素,因为 div<style> 元素的父元素。

级联中的 @scope

CSS 级联中,@scope 还添加了一个新条件:范围接近度。此步骤在具体性之后,但在显示顺序之前。

CSS 级联的可视化图表。

根据规范

比较样式规则中显示的声明时,如果这些声明具有不同的作用域根,则在作用域根和作用域样式规则正文之间具有最少代数或同级元素跳转次数的声明会胜出。

在嵌套组件的多个变体时,此新步骤非常有用。请看以下示例,它目前尚未使用 @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,即使它是应用了类 .lightdiv 的子项也是如此。这是因为此处级联使用显示顺序标准来确定胜出方。它会发现 .dark a 是最后声明的,因此它会根据 .light a 规则胜出

借助范围限定的近邻条件,此问题现已得到解决:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

由于这两个限定范围的 a 选择器具有相同的专属性,因此系统会启用限定范围的近似性标准。它会根据与其作用域根的接近程度来权衡这两个选择器。对于第三个 a 元素,到 .light 作用域根只有一个跃点,但到 .dark 作用域根有两个跃点。因此,.light 中的 a 选择器将胜出。

结束语:选择器隔离,而非样式隔离

需要注意的重要一点是,@scope 会限制选择器的范围,但不会提供样式隔离。向下继承到子项的属性仍会继承,超出 @scope 的下限。其中一个属性就是 color。在甜甜圈范围内声明一个时,color 仍会向下继承到甜甜圈洞内的子项。

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}
这个笔记

在上面的示例中,.card__content 元素及其子元素的颜色为 hotpink,因为它们从 .card 继承了值。

(封面照片:rustam burkhanov来自 Unsplash