CSS @scope

新的 @scope 规则来了!这是一种更好的方法来保持我们的组件样式封闭 - 而无需依赖第三方工具或特殊的命名约定。

作用域样式一直是 CSS 的主要目标。选择器将声明范围限定在匹配的元素上。这些选择器可以组合在一起,以创建更具体的范围 - 每个范围通过其与其他范围的关系进行修改。但是,已经在 谷歌浏览器中提供的 @scope 功能将使该功能变得更加强大。

在历史上,关系选择器在表达范围方面受到限制。像 > ‘child’ 组合选择器这样的狭窄关系,如果我们在子元素周围添加了包裹元素,它们就会破坏。而像‘descendant’组合选择器这样的广泛关系很容易渗入嵌套内容。当我们确实想要表示‘直接子代’或‘所有后代’时,这样做是可以的 - 但通常,我们试图表达的是更抽象的归属概念。

一些样式是全局的,但许多样式属于组件。当我们选择 article .title 时,我们不一定想要所有 article 内部的 .title 元素,而是特定于 article .title。这就是人们在 CSS 中要求作用域时的含义。选择器不仅传达嵌套关系,还传达归属关系。

作用域黑科技

要在 CSS 中表示归属关系,我们需要两个部分 - 正如 BEM 所称的‘块’和‘元素’。元素属于块。为了使这种关系比简单的嵌套更清晰,BEM 要求将块名称添加到作用域中的每个元素和选择器中:

1
2
3
4
5
/* 嵌套:块中的所有匹配元素 */
.block .element { … }

/* BEM:只有属于块的元素 */
.block-element { … }

现在,大多数前端框架都提供了作用域样式,会自动为元素生成唯一的块名称并将其追加到元素中:

1
2
/* 框架(细节会有所不同) */
.element[data-scope=block] { … }

但是命名约定需要组织中的严格遵守,并且自动化需要在单个构建步骤中完全控制HTMLCSS 输出。这是一个侵入性的过程,它会在整个 DOM 中应用唯一的属性,并将它们追加到每个相关选择器中。

这些都不是解决问题的可靠方案;这些是我们在过去习惯了的黑科技。

CSS @scope介绍

新的 CSS @scope 规则允许我们以两个部分定义作用域关系。首先,我们通过选择给定作用域的‘根’来定义块本身。这些选择器放在规则的‘前奏’中:

1
@scope (.block) { … }

然后,我们可以定义属于该块作用域的‘作用域’选择器:

1
2
3
@scope (.block) {
.element { … }
}

如果你使用过 CSS 嵌套,这可能会很熟悉。它将选择与以下任一相同的元素:

1
2
3
4
5
.block {
.element { … }
}

.block .element { … }

在嵌套中,每个嵌套选择器之前都有一个隐式的 &,但如果需要的话,你也可以明确地放置 && 选择器是嵌套父级的占位符:

1
2
3
4
5
.block {
.element { /* 隐式开始 & */ }
& .element { /* 显式开始 & */ }
.context & { /* 重新定位 & */ }
}

作用域的工作方式相同,但默认使用 :scope 选择器:

1
2
3
4
5
@scope (.block) {
.element { /* 隐式开始 :scope */ }
:scope .element { /* 显式开始 :scope */ }
.context :scope { /* 重新定位 :scope */ }
}

它们看起来相似,但不要被表面相似所迷惑。CSS 嵌套是编写多部分(复杂)选择器的一种简写。但尽管是以多个步骤编写的,最终的选择器只匹配一个元素:组合选择器的最终‘主体’。在选择过程中,可以引用嵌套关系来缩小我们的主体范围,但这些关系在选择完成后不会‘保留下来’。

为了使作用域在层叠中更有意义,我们需要知道我们正在样式化的元素以及它所属的块。作用域规则通过将选择显式地分成两个不同的部分,并为每个部分指定不同的目标来实现这一点:作用域根选择器和主体元素选择器。这个微妙的变化产生了巨大的不同。

作用域关联度

因为我们知道主体与其所属作用域之间的关系,我们可以做一些事情,比如测量它们的关联度 - 即它们之间的 DOM 步骤数:

1
2
3
4
5
6
7
8
9
10
11
12
<article class='block'>
<p class='element'>
距离 .element 到 .block 只有 1 步。
</p>
<footer>
<div>
<p class='element'>
从 .element 到 .block 有 3 步(通过 div 和 footer)。
</p>
</div>
</footer>
</article>

作用域关联度已经在选择器的特异性之后,但在出现顺序之前添加到了层叠中。当两个具有相同特异性的选择器应用于同一个元素时,具有“更近”的作用域根的选择器将获胜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<h1>Scope Proximity</h1>
<p class="support">
⚠️ Your browser does not support the <code>@scope</code> rule.
Try viewing this demo in Chrome Canary
with the <strong>experimental web platform features</strong> flag.
</p>
<div class="dark-theme">
<!-- <h2>with scope</h2> -->
<p>The dark-theme link should be <a href="#">lightcyan</a></p>

<div class="light-theme">
<p>The light-theme link should be <a href="#">mediumvioletred</a></p>

<div class="dark-theme">
<p>The dark-theme link should be <a href="#">lightcyan</a></p>
</div>
</div>

<p>
With <code>@scope</code>,
we can ensure that the 'nearer' scope wins,
giving us the correct result no matter how these scopes are nested.</p>
</div>

<!-- <div class="no-scope dark-theme">
<h2>no scope</h2>
<p>The dark-theme link should be <a href="#">lightcyan</a></p>

<div class="no-scope light-theme">
<p>The light-theme link should be <a href="#">mediumvioletred</a></p>

<div class="dark-theme">
<p>The dark-theme link should be <a href="#">lightcyan</a></p>
</div>
</div>

<p>
Without <code>@scope</code>,
the dark-theme link color will always win
since it is defined later in the CSS,
with the same specificity.
</p>
</div>

<p>
Scope <strong>proximity</strong>
helps resolve conflicts when two scopes overlap.
In this case, the nested/overlapping light and dark themes
both define link colors using the same selector
(and same specificity).
Without scope, the later rule takes precedence.
With scope, the 'closer' scope root has priority.
</p> -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* without scopes */
/* remove these in a supporting browser */
.light-theme a { color: mediumvioletred; }
.dark-theme a { color: lightcyan; }

/* with scopes */
@scope (.light-theme) {
:scope {
background: white;
color: black;
}
a { color: mediumvioletred; }
}

@scope (.dark-theme) {
:scope {
background: black;
color: white;
}
a { color: lightcyan; }
}

@scope (html) {
.support { display: none; }
}


.light-theme,
.dark-theme {
padding: 1em;
margin: 1em 0;
border: thin solid;
}

a:any-link {
border-left: 1em solid currentcolor;
padding-left: 0.4em;
}

/* without scopes */
.no-scope.light-theme {
background: white;
color: black;
}

.no-scope.dark-theme {
background: black;
color: white;
}

.no-scope.light-theme a:any-link { color: mediumvioletred; }
.no-scope.dark-theme a:any-link { color: lightcyan; }

.support {
border: medium solid red;
color: maroon;
padding: 1em;
}

body {
padding: 1em;
margin: 0 auto;
max-width: 70ch;
}


这在两个选择器具有相同目的并且我们可能希望有广泛重叠的作用域的情况下特别有用 - 比如从不同主题设置链接颜色。但还存在其他实现类似结果的方法,比如自定义属性(根据接近性继承)。当我们将第三个选择器添加到语法中时,事情会变得更加有趣。

作用域边界

基于组件的方法的真正强大之处在于通过以各种方式组合块来组合新模式的能力。’标签页’组件只有在我们将其他内容放在每个标签面板内时才有用。然而,标签元素不知道该内容将是什么,因此我们的’标签’组件样式的作用域应该在内容开始的地方停止。 当组件中间有一个洞(或一个插槽),用于放置其他内容

这对于嵌套选择器来说会造成问题,因为它们同样适用于所有后代。因此,我们依赖于第三方工具和 BEM 语法,以生成唯一的标识符,并将其应用于洞中的每个元素。@scope 规则使得这一点成为可能,而无需任何约定或工具。我们可以在前奏中提供第二个选择器:

1
2
3
@scope (.block) to (.slot) {
.element { … }
}

您可以在任何最新版本的 谷歌浏览器中看到这项工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<article class="media">
<img src="https://assets.codepen.io/15542/mia.jpg" alt="Miriam speaking" height="300" width="300" />
<div class="inner">
<h2 class="title">Outer Title is in-scope</h2>
<p>
This title, image,
and paragraph fall <em>between</em>
a scope-root (<code>.media</code>)
and lower-boundary (<code>.content</code>),
so the scoped styles apply.
</p>
<div class="content">
<h3 class="title">Inner Title is out-of-scope</h3>
<img src="https://assets.codepen.io/15542/sad.jpg" alt="girl, pouting" width="2220" height="1248" />
<p>
This image, title, and paragraph
are inside (<code>.media</code>),
but there is an intervening boundary
(<code>.content</code>)
that ends the scope.
</p>
</div>
</div>
</article>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* applies to both images */
img {
max-width: 100%;
height: auto;
}

@scope (.media) to (.content) {
/* only the outer image, between media and content */
img {
border-radius: 100%;
}

.title,
p {
color: rebeccapurple;
}

/* layout */
:scope {
background: lightcyan;
border: medium solid;
padding: 1em;
display: grid;
grid-template: "media content" auto / minmax(14ch, 20%) minmax(
min-content,
1fr
);
gap: 1em;
}

img {
grid-area: media;
}

.inner {
grid-area: content;
}
}

.content {
background: white;
border: medium dotted;
padding: 1em;
}

html {
height: 100%;
display: grid;
place-content: safe center;
}

body {
max-width: 75ch;
}


:not() 选择器有一个很酷的技巧,可以让你接近,但是如果开始嵌套作用域,它就会失败。创建一个孤立的代码块需要理解页面上每个根、边界和主体元素之间的 DOM 关系 - 因此需要三个不同的选择器来使其工作。

作用域规则保持权重

嵌套和 @scope 之间的另一个重要区别是选择器的优先级权重。嵌套选择器被合并为一个,具有相应的优先级权重:

1
2
3
4
#banner {
/* 优先级权重:[1, 0, 1] */
h1 { color: hotpink; }
}

但是使用作用域时,我们使用两个或三个不同的选择器。它们永远不会合并在一起,因此不会导致组合优先级权重:

1
2
3
4
@scope (#banner) {
/* 优先级权重:[0, 0, 1] */
h1 { color: hotpink; }
}

许多作者喜欢使用 BEM 语法通过使用单个类来“平铺”权重。但选择只是 CSS 的一半!如果我们只能使用类,那么我们就放弃了语言中一些最强大和表达力最强的功能。通过在合适的时候使用作用域而不是嵌套,我们可以兼得两者的优点!

使用显式的 :scope 选择器会增加一些优先级权重 - 伪类具有与普通类相同的优先级权重。它引用了作用域根元素,但没有直接引用作用域根选择器。如果我们确实想要组合这两个选择器并获得它们的组合优先级权重,我们可以使用 & 代替:

1
2
3
4
5
6
7
@scope (#banner) {
/* 权重:[0, 1, 1] */
:scope h1 { color: hotpink; }

/* 权重:[1, 0, 1] */
& h1 { color: hotpink; }
}

隐式作用域和嵌入样式

有时,直接在页面中与其样式的组件一起嵌入样式可能会很有用。在那里也可以使用作用域!没有定义根选择器的 @scope 规则将使用父元素作为隐式作用域根:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<section>
<style>
@scope to (article) {
:scope {
border: thin dotted mediumvioletred;
padding: 1em;
}

.title {
font-family: fantasy;

&::before { content: '✅ '; }
}

p { font-style: italic; }
}
</style>
<h2 class="title">Title in the section scope</h2>
<p>This paragraph is also in the section scope.</p>
<hr>
<article>
<h3 class="title">Nested article titles are not in scope</h3>
<p>
Paragraphs inside the article
are also out-of-scope!
Yay for lower boundaries!
Yay for implicit scopes!
</p>
</article>
</section>

总结

  • 使用 @scope 定义特定的模式或组件及其块元素关系。这些可以是广泛的(整个主题)或狭窄的(单个按钮),有时可能会重叠。元素属于多个作用域是可以的,但每个作用域都特定于 DOM 的某个片段。

  • 嵌套存在是为了使复杂的选择器更易读。它可以处理比 @scope 更广泛的选择器,因为它并不是设计用于一个特定的用例。虽然 @scope 在表达块元素关系方面表现出色,但在伪类等元素 - 修改器关系方面,嵌套仍然是明确的选择。

  • 使用 @layer 对不同的样式关注点进行分组和优先级排序。它们往往更广泛和建筑性 - 重置、框架、设计系统和实用程序 - 并适用于任意数量的组件。按钮组件和主题组件可能都从“默认”层开始。

  • 作用域根通常是 @container 查询的良好容器元素。我预计在许多 @scope 规则的顶部将经常看到 :scope { container: my-scope-name / inline-size; }。