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

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

浏览器支持

  • 118
  • 118
  • x
  • 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 (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ 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 级联广告素材内,@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,即使它是 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 的跳跃。因此,.light 中的 a 选择器将胜出。

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

需要注意的一点是,@scope 会限制选择器的覆盖范围,而不会提供样式隔离。向下沿用到子级的属性仍会超出 @scope 的下限。color 就是这样一种属性。在甜甜圈作用域内声明该值时,color 仍会向下继承给甜甜圈孔内的子项。

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

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

(封面照片:rustam burkhanov在 Un 划定的平台上