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

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

布拉穆斯
布拉穆斯

浏览器支持

  • 118
  • 118
  • x
  • x

编写 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 规则的范围根。

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

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

如需阻止卡片内容区域 (.card__content) 内的 <img> 元素处于选中状态,您可以使用更具体的 img 选择器。另一种方法是利用 @scope at 规则这一事实,也接受用于确定下限的范围限制

@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 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 */
  }
  :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,即使它是应用了 .light 类的 div 的子级也是如此。这是由于级联在此处用来确定胜出者的显示条件的顺序。系统发现 .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 提供)