使用 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