1. 简介
本节不具有规范性。
本模块描述了在一个样式规则内部嵌套另一个样式规则的支持,允许内部规则的选择器引用外层规则匹配的元素。此功能可以把相关的样式聚合到 CSS 文档的同一结构中,从而提升可读性和可维护性。
测试
嵌套的一般测试
- block-skipping.html (实时测试) (源码)
- delete-other-rule-crash.html (实时测试) (源码)
- double-parent-pseudo-in-placeholder-crash.html (实时测试) (源码)
- has-nesting.html (实时测试) (源码)
- invalidation-001.html (实时测试) (源码)
- invalidation-002.html (实时测试) (源码)
- invalidation-003.html (实时测试) (源码)
- invalidation-004.html (实时测试) (源码)
- nested-error-recovery.html (实时测试) (源码)
- nesting-basic.html (实时测试) (源码)
- nesting-revert-rule.tentative.html (实时测试) (源码)
- parent-pseudo-in-placeholder-crash.html (实时测试) (源码)
- pseudo-part-crash.html (实时测试) (源码)
- pseudo-where-crash.html (实时测试) (源码)
- supports-is-consistent.html (实时测试) (源码)
1.1. 模块交互
本模块引入了扩展 [CSS21] 解析模型的新解析规则。它引入了扩展 [SELECTORS-4] 模块的选择器。它也扩展并修改了 [CSSOM-1] 模块中定义的部分 IDL 与算法。
1.2. 值
本规范未定义任何新属性或新取值。
2. 说明
本节是非规范性的。
想象一下你有一些 CSS,希望以更紧凑的方式编写。
.foo{ color : green; } .foo .bar{ font-size : 1.4 rem ; }
使用嵌套,你可以这么写代码:
.foo{ color : green; .bar{ font-size : 1.4 rem ; } }
如果你以前在 Sass 或其它 CSS 预处理器中进行过嵌套样式,你会觉得这非常熟悉。
你可以在父样式规则内部嵌套任意规则。
main{ div{ ...} .bar{ ...} #baz{ ...} :has ( p) { ...} ::backdrop{ ...} [ lang|="zh" ] { ...} *{ ...} }
默认情况下,子规则的选择器被视为通过后代组合符与父规则相连,但你可以在嵌套选择器前添加任意组合符来改变这一点。
main{ + article{ ...} > p{ ...} ~ main{ ...} }
新的 & 选择器允许你显式引用父选择器匹配的元素,因此上述例子也可以写成:
main{ & + article{ ...} & > p{ ...} & ~ main{ ...} }
但你也可以把 & 放在嵌套选择器的其他位置,以表示父子规则之间的其他关系类型。例如,下面这段 CSS:
ul{ padding-left : 1 em ; } .component ul{ padding-left : 0 ; }
可以使用嵌套改写为:
ul{ padding-left : 1 em ; .component &{ padding-left : 0 ; } }
同样,& 为你提供了一种“这就是我想让嵌套选择器出现的位置”的表达方式。
当你不想在选择器之间留空格时,它也非常有用。例如:
a{ color : blue; &:hover{ color : lightblue; } }
这段代码的效果等同于 。如果没有 &,则会得到 ——注意 a 与 :hover 之间的空格——这会导致悬停链接样式失效。
你可以嵌套多层——在已经嵌套的 CSS 里再嵌套 CSS——层数不设上限。你可以随意将嵌套与容器查询、Supports 查询、媒体查询以及/或层叠层混合使用。(几乎)任何东西都可以放在任何东西内部。
3. 嵌套样式规则
样式规则可以嵌套在其他样式规则内部。这些 嵌套样式规则 的行为与普通样式规则完全相同——通过选择器将属性关联到元素——但是它们“继承”父规则的选择器上下文,允许在不重复(甚至多次重复)父选择器的情况下进一步构建选择器。
一个 嵌套样式规则 与普通样式规则相同,只是它可以使用 相对选择器,这些相对选择器隐式地相对于父规则匹配的元素。
测试
.foo{ color : red; a{ color : blue; } }
是合法的,并且等价于:
.foo{ color : red; } .foo a{ color : blue; }
嵌套规则也可以使用 嵌套选择器 直接引用父规则匹配的元素,或使用 相对选择器 语法来指定除“后代”之外的其他关系。
.foo{ color : red; &:hover{ color : blue; } } /* equivalent to: */ .foo{ color : red; } .foo:hover{ color : blue; }
.foo{ color : red; + .bar{ color : blue; } } /* equivalent to: */ .foo{ color : red; } .foo + .bar{ color : blue; }
3.1. 语法
样式规则的内容现在接受 嵌套样式规则 与 at‑rule,以及现有的 声明。
嵌套样式规则 与非嵌套规则在以下方面不同:
-
一个 嵌套样式规则 的前导部分接受 <relative-selector-list>(而不是仅仅 <selector-list>)。任何 相对选择器 都相对于由 嵌套选择器 表示的元素。
-
如果 <relative-selector-list> 中的选择器没有以组合符开头,但 包含嵌套选择器,则它被解释为非 相对选择器。
嵌套样式规则的具体解析细节定义于 [CSS‑SYNTAX‑3]。
无效的 嵌套样式规则 将被忽略,其内容也一起被忽略,但不会导致其父规则失效。
包含 相对选择器 的嵌套规则会把它们暗含的 嵌套选择器 的特异性计入。例如,.foo { > .bar {...}} 与 .foo { & > .bar {...}} 对其内部规则的特异性是相同的。
例如,若一个组件使用类 .foo,而一个嵌套组件使用 .fooBar,在 Sass 中可以这样写:
.foo{ color : blue; &Bar{ color : red; } } /* In Sass, this is equivalent to .foo { color: blue; } .fooBar { color: red; } */
这在 CSS 中是不允许的,因为嵌套并不是一种语法转换,而是直接匹配父选择器匹配到的实际元素。
同样,选择器 &Bar 在 CSS 中本身就是非法的,因为 Bar 部分是类型选择器,必须位于复合选择器的最前面(即应写作 Bar&)。所以幸运的是,CSS Nesting 与预处理器语法之间不存在冲突。
注意:此表述方式是为了捕获诸如 :is(:unknown(&), .bar) 这类情形——其中的未知选择器(因为未知,我们无法确定参数是否应该被解析为选择器)是唯一包含 & 的部分。由于这可能是只有新浏览器才支持的完全合法选择器,而我们不希望解析行为受无关版本问题的影响,故仍将其视为包含嵌套选择器。
如果<forgiving-selector-list>中的某个项包含嵌套选择器但本身无效,则该项会原样保留而不是被丢弃。(这不会改变选择器的匹配行为——无效选择器仍然匹配不到任何元素——但会影响选择器的序列化方式。)
前面的段落需要在我们把 & 本身迁移到 Selectors 模块时一起迁移;这里的写法是为方便而做的临时补丁。
3.2. 示例
/* & can be used on its own */ .foo{ color : blue; & > .bar{ color : red; } > .baz{ color : green; } } /* equivalent to .foo { color: blue; } .foo > .bar { color: red; } .foo > .baz { color: green; } */ /* or in a compound selector, refining the parent’s selector */ .foo{ color : blue; &.bar{ color : red; } } /* equivalent to .foo { color: blue; } .foo.bar { color: red; } */ /* multiple selectors in the list are all relative to the parent */ .foo, .bar{ color : blue; + .baz, &.qux{ color : red; } } /* equivalent to .foo, .bar { color: blue; } :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { color: red; } */ /* & can be used multiple times in a single selector */ .foo{ color : blue; & .bar & .baz & .qux{ color : red; } } /* equivalent to .foo { color: blue; } .foo .bar .foo .baz .foo .qux { color: red; } */ /* & doesn’t have to be at the beginning of the selector */ .foo{ color : red; .parent &{ color : blue; } } /* equivalent to .foo { color: red; } .parent .foo { color: blue; } */ .foo{ color : red; :not ( &) { color : blue; } } /* equivalent to .foo { color: red; } :not(.foo) { color: blue; } */ /* But if you use a relative selector , an initial & is implied automatically */ .foo{ color : red; + .bar + &{ color : blue; } } /* equivalent to .foo { color: red; } .foo + .bar + .foo { color: blue; } */ /* Somewhat silly, but & can be used all on its own, as well. */ .foo{ color : blue; &{ padding : 2 ch ; } } /* equivalent to .foo { color: blue; } .foo { padding: 2ch; } // or .foo { color: blue; padding: 2ch; } */ /* Again, silly, but can even be doubled up. */ .foo{ color : blue; &&{ padding : 2 ch ; } } /* equivalent to .foo { color: blue; } .foo.foo { padding: 2ch; } */ /* The parent selector can be arbitrarily complicated */ .error, #404{ &:hover > .baz{ color : red; } } /* equivalent to :is(.error, #404):hover > .baz { color: red; } */ .ancestor .el{ .other-ancestor &{ color : red; } } /* equivalent to .other-ancestor :is(.ancestor .el) { color: red; } /* As can the nested selector */ .foo{ &:is ( .bar, &.baz) { color : red; } } /* equivalent to .foo :is(.bar, .foo.baz) { color: red; } */ /* Multiple levels of nesting "stack up" the selectors */ figure{ margin : 0 ; > figcaption{ background : hsl ( 0 0 % 0 % /50 % ); > p{ font-size : .9 rem ; } } } /* equivalent to figure { margin: 0; } figure > figcaption { background: hsl(0 0% 0% / 50%); } figure > figcaption > p { font-size: .9rem; } */ /* Example usage with Cascade Layers */ @layer base{ html{ block-size : 100 % ; body{ min-block-size : 100 % ; } } } /* equivalent to @layer base { html { block-size: 100%; } html body { min-block-size: 100%; } } */ /* Example nesting Cascade Layers */ @layer base{ html{ block-size : 100 % ; @layer support{ body{ min-block-size : 100 % ; } } } } /* equivalent to @layer base { html { block-size: 100%; } } @layer base.support { html body { min-block-size: 100%; } } */ /* Example usage with Scoping */ @scope ( .card) to( > header) { :scope{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; > header{ border-block-end : 1 px solid white; } } } /* equivalent to @scope (.card) to (> header) { :scope { inline-size: 40ch; aspect-ratio: 3/4; } :scope > header { border-block-end: 1px solid white; } } */ /* Example nesting Scoping */ .card{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; @scope ( &) to( > header > *) { :scope > header{ border-block-end : 1 px solid white; } } } /* equivalent to .card { inline-size: 40ch; aspect-ratio: 3/4; } @scope (.card) to (> header > *) { :scope > header { border-block-end: 1px solid white; } } */
3.3. 嵌套其他 At-规则
除了嵌套样式规则之外,本规范还允许在样式规则内部嵌套嵌套组规则:任何其主体包含样式规则的 at‑rule 均可以嵌套在样式规则内部,除非另有规定。
以这种方式嵌套时,嵌套组规则块的内容会被解析为 <block-contents>,而不是 <rule-list>。
测试
此类嵌套组规则的含义与行为保持不变,除非另有规定。
/* Properties can be directly used */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; } } /* equivalent to: */ .foo{ display : grid; } @media ( orientation: landscape) { .foo{ grid-auto-flow : column} } /* and also equivalent to the unnested: */ .foo{ display : grid; } @media ( orientation: landscape) { .foo{ grid-auto-flow : column; } } /* Conditionals can be further nested */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; @media ( min-width >1024 px ) { max-inline-size : 1024 px ; } } } /* equivalent to */ .foo{ display : grid; } @media ( orientation: landscape) { .foo{ grid-auto-flow : column; } } @media ( orientation: landscape) and( min-width >1024 px ) { .foo{ max-inline-size : 1024 px ; } } /* Example nesting Cascade Layers */ html{ @layer base{ block-size : 100 % ; @layer support{ & body{ min-block-size : 100 % ; } } } } /* equivalent to */ @layer base{ html{ block-size : 100 % ; } } @layer base.support{ html body{ min-block-size : 100 % ; } } /* Example nesting Scoping */ .card{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; @scope ( &) { :scope{ border : 1 px solid white; } } } /* equivalent to */ .card{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; } @scope ( .card) { :scope{ border-block-end : 1 px solid white; } }
连续直接嵌套的属性会自动被包装进嵌套声明规则。(这在 CSSOM 中可见。)
3.3.1. 嵌套 @scope 规则
当 @scope 规则本身是嵌套组规则时,& 在 <scope-start> 选择器中指向最近祖先样式规则匹配的元素。
.parent{ color : blue; @scope ( & > .scope) to( & .limit) { & .content{ color : red; } } }
等同于
.parent{ color : blue; } @scope ( .parent > .scope) to( :where ( :scope) .limit) { :where ( :scope) .content{ color : red; } }
3.4. 混合嵌套规则和声明
当一个样式规则同时包含声明以及嵌套样式规则或嵌套组规则时,三者可以任意混合。出现在规则之后或其间的声明会隐式包装进嵌套声明规则,以保持相对于其他规则的顺序。
article{ color : green; &{ color : blue; } color: red; } /* equivalent to */ article{ color : green; } :is ( article) { color : blue; } article{ color : red; } /* NOT equivalent to */ article{ color : green; } article{ color : red; } :is ( article) { color : blue; }
为了确定出现顺序,嵌套样式规则和嵌套组规则被视为出现在它们的父规则之后。
article{ color : blue; &{ color : red; } }
两个声明的特异性相同 (0,0,1),但由于嵌套规则被视作出现在父规则之后,color: red 的声明在层叠中获胜。
相反,在下面的例子中:
article{ color : blue; :where ( &) { color : red; } }
:where() 伪类把嵌套选择器的特异性降为 0,因此color: red 现在的特异性为 (0,0,0),在“出现顺序”起作用之前会被 color: blue 的声明覆盖。
注意:虽然可以随意在同一规则中交叉出现声明和嵌套规则,但这样会降低可读性并且容易产生混淆,因为后面的属性会被自动包装进一个在源码中不可见的嵌套声明规则。为提升可读性,建议作者先把所有属性写在规则的最前面,再写任何嵌套规则。(这在旧的用户代理中也表现更好:由于解析和错误恢复的细节,嵌套规则之后的属性可能会被跳过。)
4. 嵌套选择器:& 选择器
在使用嵌套样式规则时,必须能够引用父规则匹配的元素;这正是嵌套的全部意义。为此,本规范定义了一个新选择器——嵌套选择器,其写法为 &(U+0026 AMPERSAND)。
当它出现在嵌套样式规则的选择器中时,嵌套选择器代表父规则匹配的元素。当它出现在其他任何上下文时,它的行为等同于该上下文中的:scope(除非另有定义)。
a, b{ & c{ color : blue; } }
等同于
:is ( a, b) c{ color : blue; }
.foo, .foo::before, .foo::after{ color : red; &:hover{ color : blue; } }
这里的 & 仅代表 .foo 匹配的元素;换句话说,它等价于
.foo, .foo::before, .foo::after{ color : red; } .foo:hover{ color : blue; }
我们希望放宽此限制,但必须同时对 :is() 与 & 做同样的处理,因为它们是基于相同底层机制构建的。(Issue 7433)
特异性 由嵌套选择器的父选择器列表中**最大**的复合选择器特异性决定(行为同:is()),如果不存在选择器列表,则为 0。
#a, b{ & c{ color : blue; } } .foo c{ color : red; }
在如下的 DOM 结构中:
< b class = foo > < c > Blue text</ c > </ b >
文本将呈现为蓝色而不是红色。&的特异性取 #a([1,0,0])与 b([0,0,1])两者中较大的,即 [1,0,0],因此整个 & c 选择器的特异性为 [1,0,1],大于 .foo c([0,1,1])。
值得注意的是,这与把嵌套手动展开为非嵌套规则的结果**不同**,因为此时 color: blue 的声明是通过 b c([0,0,2])匹配,而不是 #a c([1,0,1])。
为何特异性会与非嵌套规则不同?
嵌套选择器特意使用与:is() 伪类相同的特异性规则:只取其参数中**最大的**特异性,而不追踪哪一个选择器实际匹配。
这出于性能考虑;如果一个选择器可能拥有多种特异性,取决于匹配的精确程度,会导致选择器匹配过程极其复杂且缓慢。
这引出了另一个问题:我们为何要用 & 来定义,而不是直接使用 :is()?一些非浏览器实现的类 Nesting 功能并未把它们去糖为 :is(),主要是因为它们出现在 :is() 引入之前。这些实现直接去糖,但这会导致**显著**的问题:在某些(相当常见)的情况下会意外生成**极大**的选择器,因为可能性的指数级爆炸。
.a1, .a2, .a3{ .b1, .b2, .b3{ .c1, .c2, .c3{ ...; } } } /* naively desugars to */ .a1 .b1 .c1, .a1 .b1 .c2, .a1 .b1 .c3, .a1 .b2 .c1, .a1 .b2 .c2, .a1 .b2 .c3, .a1 .b3 .c1, .a1 .b3 .c2, .a1 .b3 .c3, .a2 .b1 .c1, .a2 .b1 .c2, .a2 .b1 .c3, .a2 .b2 .c1, .a2 .b2 .c2, .a2 .b2 .c3, .a2 .b3 .c1, .a2 .b3 .c2, .a2 .b3 .c3, .a3 .b1 .c1, .a3 .b1 .c2, .a3 .b1 .c3, .a3 .b2 .c1, .a3 .b2 .c2, .a3 .b2 .c3, .a3 .b3 .c1, .a3 .b3 .c2, .a3 .b3 .c3{ ...}
在这里,三层嵌套且每层列表中有三个选择器,会产生 27 条去糖后的选择器。若向列表中再添加选择器、增加嵌套层级或让嵌套规则更复杂,可能会让一个相对较小的规则展开成数兆字节的选择器(甚至更多!)。
部分 CSS 工具通过启发式地丢弃一些变体来避免最坏情况,从而不必输出如此庞大的选择器,且仍然大体上正确,但这对用户代理来说并不是一种可选方案。
改用 :is() 去糖则完全消除了这个问题,代价是让特异性稍微不那么“可用”,这被认为是一个合理的权衡。
虽然嵌套选择器在复合选择器中的位置不会影响其行为(即 &.foo 与 .foo& 匹配相同元素),但已有规则规定,如果出现类型选择器,它必须放在复合选择器的最前面仍然适用(即 &div 是非法的,必须写成 div&)。
测试
- contextually-invalid-selectors-001.html (live test) (source)
- contextually-invalid-selectors-002.html (live test) (source)
- contextually-invalid-selectors-003.html (live test) (source)
- host-nesting-001.html (live test) (source)
- host-nesting-002.html (live test) (source)
- host-nesting-003.html (live test) (source)
- host-nesting-004.html (live test) (source)
- host-nesting-005.html (live test) (source)
- nesting-type-selector.html (live test) (source)
5. 嵌套声明规则
出于一些技术原因,能够区分出现在样式规则内容开头的属性和那些夹杂在其他规则之间的属性是很重要的。
.foo{ color : red; @media ( ...) { ...} background: blue; }
我们需要对 color: red 和 background: blue 稍作区别。特别是,在 CSSOM 中,color: red 作为 style 属性公开,而 background: blue 则必须出现在其 cssRules 列表中。
为实现此目的,CSS 解析会自动将这些属性包装在一个特殊的子规则中以容纳它们。然而,如果我们将它们包装在带有 & 选择器的style rule 中,会产生一些不理想的行为。
.foo, .foo::before{ color : red; &{ background : blue; } }
嵌套规则 不会 将 background 属性应用于 .foo::before 元素,因为 & 不能表示伪元素。
同样,嵌套的非样式规则中的子声明需要以某种方式暴露为规则,因为这类规则(如@media)从未拥有 style 属性。这会遇到与上述相同的问题。
为了解决所有这些问题,我们改为将连续直接嵌套的属性序列包装在一个 嵌套声明规则 中。
除非另有说明,嵌套声明规则 是 嵌套样式规则,并且行为与任何其他样式规则完全相同。它匹配的元素和伪元素与其父样式规则完全一致,且具有相同的特异性行为。(这 类似于 使用带有 & 选择器的样式规则,但如上所述更加强大。)
嵌套声明规则为何存在?
最初,本规范将所有样式规则中的声明组合在一起,“移动”它们到规则的前部。它还会自动将原始声明包装在 嵌套组规则 中的普通样式规则里,使用 & 选择器。
我们改用 嵌套声明规则 的两个主要原因如下。
首先,使用 & {...} 规则隐式地将声明包装在 嵌套组规则 中也会改变行为。如本注释后的示例所示,它会破坏父样式规则包含伪元素的情况,即使不是这种情况,也可能改变嵌套声明的特异性行为。改用 嵌套声明规则 可避免这些问题,使嵌套的 @media/等行为与非嵌套的 @media/等完全相同。
其次,未来 CSS 功能(尤其是“mixins”)的细节若声明交错自动移动到样式规则前部将无法正常工作。我们需要保持它们与其他规则的相对顺序,为了在 CSSOM 中能够表示,这意味着必须将它们包装在某种规则中。如果仅使用普通的 & {...} 规则,同样会出现前段所述的问题,因此 嵌套声明规则 让我们能够在不产生副作用的前提下实现。
.foo, .foo::before, .foo::after{ color : black; @media ( prefers-color-scheme: dark) { &{ color : white; } } }
在暗色模式页面中,.foo 元素的文字颜色会被改为白色,但它的::before 和 ::after 伪元素仍保持黑色,因为& 选择器不能表示伪元素。
然而,它被写成了
.foo, .foo::before, .foo::after{ color : black; @media ( prefers-color-scheme: dark) { color : white; } }
于是,color: white 被隐式包装在 嵌套声明规则 中,该规则保证与其父样式规则 **完全相同** 地匹配,所以元素 **以及** 其伪元素在暗色模式页面中都会显示白色文字。
.foo{ color : black; @media ( ...) { ...} background: silver; }
如果检查 .foo 规则的 CSSOM 对象,其 style 属性将只包含一个声明:color: black。
background: silver 声明则会出现在隐式创建的 嵌套声明子规则 中,位于 fooRule。
测试
- mixed-declarations-rules.html (visual test) (source)
- nested-declarations-cssom-whitespace.html (live test) (source)
- nested-declarations-cssom.html (live test) (source)
- nested-declarations-matching.html (live test) (source)
6. CSSOM
注意: [CSSOM-1] 现在定义 CSSStyleRule 可以拥有子规则。
在序列化相对选择器于嵌套样式规则时,必须将选择器绝对化,并插入隐含的嵌套选择器。
6.1. The CSSNestedDeclarations Interface
The CSSNestedDeclarations interface represents a 嵌套声明规则。
[Exposed =Window ]interface :CSSNestedDeclarations CSSRule { [SameObject ,PutForwards =cssText ]readonly attribute CSSStyleProperties style ; };
style attribute must return a CSSStyleProperties object for the rule, with the following properties- 计算标志
-
未设定
- 只读标志
-
未设定
- declarations
-
The declared declarations in the rule, in specified order.
- 父 CSS 规则
- 所有者节点
-
Null
The CSSNestedDeclarations rule serializes as if its declaration block had been serialized directly.
注意: 这意味着多个相邻的 嵌套声明规则(可通过例如 insertRule)在序列化后并再次解析时会合并为单个规则。
隐私考量
本规范未报告任何新的隐私考量。
安全考量
本规范未报告任何新的安全考量。
7. 更改
自 2023 年 2 月 14 日工作草案 以来的重大更改
-
The <scope-start> selector of a @scope rule no longer acts as the parent rule for nesting. (Issue 9740)
-
澄清了嵌套选择器允许匹配无特征元素。
-
将 &div 再次设为无效;由于语法现在支持“无限前瞻”,我们不再需要允许它。此外,这样做可以避免与预处理器的冲突。 (Issue 8662)
-
CSSOM 现在定义
CSSStyleRule为CSSGroupingRule的子类,因此手动定义cssRules属性及相关机制已被移除。 (Issue 8940) -
澄清了隐含的嵌套选择器对特异性的影响。 (Issue 9069)
-
与规则交错的声明(或嵌套组规则中的所有声明)现在会自动包装在
规则中。(同时添加了@nest 规则。) (Issue 8738)@nest -
用嵌套声明规则取代了
。(Issue 10234)@nest -
添加了 Web 平台测试覆盖。