首页 CSSDAY2 Selectors And Cascade: Cascade
文章
取消

CSSDAY2 Selectors And Cascade: Cascade

开始根据林想的Web工坊大佬的建议路线学习CSS。

本文涉及内容会如下:

CSS selectors - CSS | MDN

CSS cascading and inheritance - CSS | MDN

级联(或层叠,Cascade)是一种算法,用于定义用户代理(User Agents)如何整合源自不同来源的属性值。

当多条 CSS 规则同时作用于同一个元素时,级联算法决定最终哪条声明生效。CSS 声明来自不同的来源类型,且在同一来源内还可以分为不同的层(@layer),它们之间的默认作用域有重叠,级联算法就是用来定义这些来源和层之间如何交互的。

Introduction

Origin types

CSS 声明有三个来源:

User-agent stylesheets(浏览器样式表)

浏览器内置的默认样式表。例如 <h1> 默认加粗等、这些都是 user-agent 样式。不同浏览器的默认样式有差异,开发者常使用 CSS reset(如 normalize.css)将各浏览器默认样式统一到同一基线。

除非 user-agent 样式使用了 !important,否则 author 样式(包括 reset)始终优先于 user-agent 样式,无论选择器特异性如何。

Author stylesheets(作者样式表)

开发者写的样式,包括外链 .css 文件、<style> 块、以及元素的 style 属性(inline style)。Author 样式定义了网站的外观和主题,是最常见的样式来源。

User stylesheets(用户样式表)

网站用户可以通过浏览器配置或浏览器扩展注入自定义样式(如高对比度模式、字体大小覆盖),用来定制自己的浏览体验。

Cascade layers(级联层)

在每个来源类型内部,样式还可以用 @layer@import layer() 分到不同的级联层中。未分配到任何层的样式被视为属于一个匿名的”最后声明的层”。层的声明顺序决定了优先级,后面会详细展开。

Cascading order(级联顺序)

级联算法按以下 5 步逐步筛选,每一步淘汰不满足的声明,最终确定每个属性的唯一值:

Step 1: Relevance(相关性)

过滤掉不适用于当前元素的规则:选择器不匹配的、或 @media 条件不满足的(如 @media print 在屏幕浏览时不参与级联)。

@ 开头的是 at-rule(规则指令),是 CSS 的元指令,不是普通样式声明,可以按条件决定是否应用某段样式。@media 和 @layer 等都是指令型的 CSS 语法,所以统一用 @ 前缀。

Step 2: Origin and importance(来源与重要性)

按规则来源和是否 !important 排优先级。这个阶段会将所有声明分为normal和important两波,分别进行排序,会先忽略层的概念,级联顺序如下(从低到高):

优先级来源重要性
1user-agent(浏览器)normal
2user(用户)normal
3author(开发者)normal
4CSS keyframe animations
5author(开发者)!important
6user(用户)!important
7user-agent(浏览器)!important
8CSS transitions
1
2
3
4
5
6
7
8
9
第一拨(normal):
 所有没标 !important 的声明
 → 按来源优先级排:author > user > UA
 → 决出胜者,先应用

第二拨(!important):
 所有标了 !important 的声明
 → 按来源优先级排(反转):UA > user > author
 → 决出胜者,覆盖第一拨的结果(如果设置了相同属性)

几个关键规律:

  • normal 阶段:author > user > user-agent,开发者写的样式优先级最高
  • !important 阶段:优先级反转为 user-agent > user > author,因为设计意图是将 !important 作为”紧急出口”,用户的无障碍需求(如高对比度)和浏览器的基本可用性应优先于开发者的设计意图
  • animations 优先于所有 normal 声明
  • transitions 优先于一切,包括 !important

@layer 决定的优先级也在 Step 2 进行决断,具体优先级排列根据先后声明不同有所区别。未分配到任何层的最后声明层的优先级决断较为特殊:

阶段层顺序未分层位置
normal先声明 < 后声明最后(最高优先级)
!important先声明 > 后声明(反转)最前(最低优先级)

级联算法在特异性算法之前运行。即使 user stylesheet 的选择器特异性更高,只要 author stylesheet 在来源优先级上更高,user 的声明就会在 Step 2 被淘汰,特异性根本没有机会参与比较。

Step 3: Specificity(特异性)

同一来源 + 同一重要性下,比较选择器的特异性分数,高的胜出。

Step 4: Scoping proximity(作用域接近度)

特异性相同时,@scope 规则中距作用域根 DOM 层级跳数少的胜出。

Step 5: Order of appearance(出现顺序)

以上全部相同时,后声明的覆盖先声明的

Basic example(基础示例)

通过一个完整例子走一遍级联算法的 5 步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* User-agent CSS */
li { margin-left: 10px; }

/* Author CSS 1 */
li { margin-left: 0; } /* reset */

/* Author CSS 2 */
/* 平时看到的所有网页样式都属于 screen */
@media screen {
  li { margin-left: 3px; }
}
/* 只在打印时生效,屏幕上看不到 */
@media print {
  li { margin-left: 1px; }
}
@layer namedLayer {
  li { margin-left: 5px; }
}

/* User CSS */
.specific { margin-left: 1em; }
1
2
3
4
<ul>
  <li class="specific">1<sup>st</sup></li>
  <li>2<sup>nd</sup></li>
</ul>

Step 1 Relevance@media print 中的 1px 不适用于屏幕浏览,淘汰。

Step 2 Origin and importance:没有 !important,所以 author > user > user-agent。User CSS 的 1em 和 User-agent 的 10px 在来源优先级上低于 author,被淘汰。

注意:虽然 .specific 的特异性高于 li,但特异性在 Step 3 才参与比较,此时 user 声明已在 Step 2 被淘汰了。同样,@layer namedLayer 中的 5px 属于分层样式,normal 分层样式优先级低于同来源的未分层样式,也被淘汰。

剩下 Author CSS 中的两条未分层声明:03px

Step 3 Specificity:两条选择器都是 li,特异性相同。

Step 4 Scoping proximity:都不在 @scope 内,不适用。

Step 5 Order of appearance3px 后声明,胜出。

最终结果margin-left: 3px

Origin and importance 步骤在 specificity 之前执行。User CSS 的 .specific 虽然特异性更高,但它在 Step 2 就被淘汰了,特异性根本没机会上场。

分层样式同理,虽然 @layer namedLayer 中的声明在代码中靠后,但 normal 分层样式的优先级低于未分层样式,也在 Step 2 被淘汰。Order of appearance 只有在来源、重要性和特异性全部相同时才起作用。

Author styles: inline styles, layers, and precedence(作者样式中的内联样式、层与优先级)

级联顺序的完整表格在每个来源类型内部还细分了级联层和 inline style。以 Author 样式为例:

这个例子先利用 @import 规则在 <style> 信息元素中导入了五个外部样式表:

1
2
3
4
5
6
7
8
9
10
11
<style>
  @import "unlayeredStyles.css";
  @import "AStyles.css" layer(A);
  @import "moreUnlayeredStyles.css";
  @import "BStyles.css" layer(B);
  @import "CStyles.css" layer(C);
  p {
    color: red;
    padding: 1em !important;
  }
</style>

在文档的正文,这个例子有内联样式:

1
<p style="line-height: 1.6em; text-decoration: overline !important;">Hello</p>

三个级联层 A、B、C 按声明顺序创建,还有两个未分层的样式表和两个 inline 声明(一个是普通声明 line-height,一个是重要声明 text-decoration)。Author 样式内部的完整优先级如下(从低到高):

优先级Author 样式重要性
1A — 第一个层normal
2B — 第二个层normal
3C — 最后一个层normal
4所有未分层样式normal
5inline stylenormal
6animations
7未分层样式!important
8C — 最后一个层!important
9B — 第二个层!important
10A — 第一个层!important
11inline style!important
12transitions

关键规律:

  • normal 阶段:后声明的层 > 先声明的层 > 未分层 > inline style。分层样式的优先级最低。即使层内选择器特异性再高(如 :root body p),也会因为 normal 分层样式优先级低于未分层样式而被淘汰。只有当高特异性选择器也在未分层样式中时,特异性比较才会生效。
  • !important 阶段:优先级反转,先声明的层的 !important > 后声明的层的 !important > 未分层的 !important

Inline styles(内联样式)

inline 在 Author 内部永远是最高优先级。!important 反转的是层之间来源之间的顺序。但对 inline 来说,它在 Author 内部始终最高,不受反转影响。

Inline style 只存在于 Author 样式中。Normal inline style 的优先级高于所有其他 normal author 样式,无论选择器特异性如何。但 normal inline style 不会覆盖 animation 或 transition 正在设置的属性值。

Important inline style 优先于所有其他 author 样式(包括其他 !important 声明和分层声明),也优先于 animation 属性,但不优先于 transition 属性。只有三样东西能覆盖 important inline style:

  1. Important user style
  2. Important user-agent style
  3. Transitioned property

Importance and layers(重要性与层级)

!important 会反转级联层的优先级。举例:

1
2
3
4
5
/* normal 阶段:未分层 > 分层 → 段落红色 */
p { color: red; }
@layer B {
  :root p { color: blue; }
}

虽然 @layer B 中的选择器特异性更高,但 normal 分层样式优先级低于未分层样式,所以段落是红色。

加上 !important 后,优先级反转:

1
2
3
4
5
/* !important 阶段:分层 > 未分层 → 段落蓝色 */
p { color: red !important; }
@layer B {
  :root p { color: blue !important; }
}

@layer B 中先声明的层的 !important 优先级更高,所以段落是蓝色。如果 inline style 也加了 !important(如 <p style="color: black !important">),则段落会是黑色,因为inline !important 优先于所有 author 的 !important

正因 !important 会反转层级优先级,不要用 !important 来覆盖外部样式。正确做法是用 @import layer() 将外部样式表(框架、组件库等)导入到层中,作为 CSS 中最先声明的层,降低其优先级,后续自己定义的层优先级自然更高。

!important 只应谨慎地用在最先声明的层中,用于保护关键样式不被后续覆盖。

Complete cascade order(完整级联顺序)

综合来源类型、级联层和 inline style,完整的级联优先级如下(从低到高):

优先级样式来源重要性
1user-agent — 第一个声明的层normal
 user-agent — 最后一个声明的层normal
 user-agent — 未分层样式normal
2user — 第一个声明的层normal
 user — 最后一个声明的层normal
 user — 未分层样式normal
3author — 第一个声明的层normal
 author — 最后一个声明的层normal
 author — 未分层样式normal
 inline stylenormal
4animations
5author — 未分层样式!important
 author — 最后一个声明的层!important
 author — 第一个声明的层!important
 inline style!important
6user — 未分层样式!important
 user — 最后一个声明的层!important
 user — 第一个声明的层!important
7user-agent — 未分层样式!important
 user-agent — 最后一个声明的层!important
 user-agent — 第一个声明的层!important
8transitions

!important 反转的是层之间来源之间的顺序。但对 inline 来说,它在 Author 内部始终最高,不受反转影响。

每个 !important 区间内部,层的优先级与 normal 阶段相反:先声明的层 > 后声明的层 > 未分层。来源类型之间的反转同理:user-agent !important > user !important > author !important

Which CSS entities participate in the cascade(哪些 CSS 实体参与级联)

只有 CSS 属性/值声明 参与级联。以下不参与:

  • @font-face 中的描述符:整个 @font-face 规则作为整体参与级联,内部的描述符(如 font-family)不参与。如果多个 @font-face 规则定义了相同的字体名,只有最合适的那一个被选中;若多个同等合适,则整个规则按级联步骤 1、2、4 比较(at-rule 没有特异性这一步)。
  • @keyframes 中的声明:整个 @keyframes 规则作为整体参与级联,内部的关键帧声明不参与。
  • HTML 展示属性:如 <font color="red">,不属于级联体系(但作为低优先级的单独来源处理)。

虽然 @media@supports 等规则内部的声明参与级联,但这些 at-rule 本身可能使整个选择器不相关(如屏幕浏览时 @media print 中的规则在 Step 1 就被过滤掉了)。

CSS animations and the cascade(CSS 动画与级联)

@keyframes 不参与级联。这意味着在任何给定时间,CSS 只从一个 @keyframes 集合中取值,不会混合多个集合

关键帧的用途很单纯,仅仅是定义一组描述状态变化的关键帧,然后负责被animation属性引用。

如果多个 @keyframes 定义了相同的动画名称(animation-name),只有来源和层级优先级最高的那一个集合被使用,其余的 @keyframes 被完全忽略,即使它们动画化的属性不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
p {
  animation: infinite 5s alternate repeatedName;
}

@keyframes repeatedName {
  from { font-size: 1rem; }
  to { font-size: 3rem; }
}

@layer A {
  @keyframes repeatedName {
    from { background-color: yellow; }
    to { background-color: orange; }
 }
}

@layer B {
  @keyframes repeatedName {
    from { color: white; }
    to { color: black; }
  }
}

这里有三个同名的 @keyframes 声明。根据来源和层级优先级规则,未分层 CSS 中的关键帧优先于分层中的关键帧,所以最终只有 font-size 的动画生效,background-colorcolor 的动画被完全忽略,不会被混合。

@keyframes 块中如果属性值包含 !important,整个声明会被忽略,不存在 “important animations”。


Resetting styles(重置样式)

内容在完成对样式的修改后(如动画结束、主题切换等),可能需要将样式恢复到某个已知状态。CSS 的 all 属性可以让你快速将(几乎)所有属性重置。

all 属性接受以下值:

效果
all: initial所有属性恢复到各自的初始值(CSS 规范定义的默认值)
all: inherit所有属性改为继承父元素的值
all: unset可继承属性视为 inherit,不可继承属性视为 initial
all: revert回到级联中上一层来源的值(author 回 user-agent 默认;layer 回外层 layer)
all: revert-layer回到上一层级联层的值
1
2
3
4
/* 动画结束后重置所有属性 */
.element {
  all: revert;
}

all 的关键用途:当开发者的层级在某组件上堆了大量样式后,想彻底清空回到某个干净状态,一行 all: unsetall: revert 就搞定了,不需要逐属性重置。

Inheritance

CSS 属性分为两类:可继承的不可继承的。当一个元素没有对某个属性显式设值时,浏览器根据属性类型决定从哪里取值。

Inherited properties(可继承属性)

没有显式设值时,元素从父元素获取该属性的计算值。只有文档根元素(<html>)才会获取属性的初始值(CSS 规范定义的默认值)。

常见可继承属性:colorfont-familyfont-sizeline-heighttext-alignvisibilitycursor 等,大多是文字和排版相关

1
p { color: green; }
1
<p>This paragraph has <em>emphasized text</em> in it.</p>

<em> 自己没有设 color → 从父元素 ``继承 color: green → “emphasized text” 也是绿色。它不会去拿 CSS 规范定义的初始色(通常是黑色)。


Non-inherited properties(不可继承属性)

没有显式设值时,元素获取的是 CSS 规范定义的初始值,不会去父元素找。

常见不可继承属性:bordermarginpaddingwidthheightbackgroundpositiondisplay 等,大多是盒模型和布局相关

1
p { border: medium solid; }
1
<p>This paragraph has <em>emphasized text</em> in it.</p>

<em> 自己没有设 border → 不去父元素 p 找 → 用 border-style 的初始值 none → 没有边框。


手动覆盖继承行为

inherit 强制继承

让不可继承的属性也去父元素取值:

1
2
p { border: medium solid; }
em { border: inherit; } /* em 从 p 继承边框 */
1
<p>This paragraph has <em>emphasized text</em> in it.</p>

现在 “emphasized text” 也有边框了,<em> 强制从 <p> 继承了 border: medium solid

inherit 对可继承属性也生效,效果就是显式确认”我要父元素的值”。

all 简写: 一键控制所有属性

回到上一节提到的 all 属性,可以有效重置继承状态:

1
2
3
4
5
p {
  all: revert; /* 全部回到浏览器默认(或用户样式表) */
  font-size: 200%;
  font-weight: bold;
}

可继承不可继承简单对比

类型没设值时从哪取常见属性
可继承父元素的计算值colorfont-*text-*line-heightvisibility
不可继承CSS 规范的初始值bordermarginpaddingwidthdisplay

文字排版相关属性大多可继承(从父拿),盒模型/布局属性大多不可继承(从规范初始值拿)。inherit 可以打破这个规则,all 可以一键重置。

Specificity

特异性是赋予每条 CSS 声明的权重分数。当多条声明为同一元素设置同一属性时,匹配该元素的选择器中权重最高者的声明生效。

How is specificity calculated?

特异性是一个三栏的值:ID - CLASS - TYPE,从左到右逐栏比较。

Selector weight categories(选择器权重类别)

权重栏包含的选择器每个计为
IDID selector(如 #example1-0-0
CLASSclass 选择器(.myClass)、属性选择器([type="radio"])、伪类(:hover:nth-of-type(3n):required0-1-0
TYPEtype 选择器(ph1)、伪元素(::before::placeholder0-0-1
无权重universal selector *:where() 及其参数0-0-0

组合器(+>~、空格、||)不增加特异性权重。& 嵌套组合器也不增加,但嵌套规则自身会按其内容计算权重,行为和 :is() 一致。

Three-column comparison(三栏逐位比较)

从最左栏(ID)开始比较,高者胜出。同分则看下一栏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 1个 ID 大于 4个 class */
#myElement { color: green; } /* 1-0-0 — 胜出 */
.bodyClass .sectionClass .parentClass [id="myElement"] { color: yellow; } /* 0-4-0 */

/* ID 相同,比 CLASS */
#myElement { color: yellow; } /* 1-0-0 */
#myApp [id="myElement"] { color: green; } /* 1-1-0 — 胜出 */

/* ID 和 CLASS 相同,比 TYPE */
:root input { color: green; } /* 0-1-1 — 胜出(CLASS 栏就赢了) */
html body main input { color: yellow; } /* 0-0-4 */

/* 三栏全部相同,后声明的胜出 */
input.myClass { color: yellow; } /* 0-1-1 */
:root input { color: green; } /* 0-1-1 — 后声明,胜出 */

Matching selector(匹配选择器)

在 selector list 中,特异性取自实际匹配该元素的选择器中权重最高者

1
2
3
4
5
[type="password"],
input:focus,
:root #myApp input:required {
  color: blue;
}
  • 普通密码输入 → 只匹配 [type="password"],特异性 0-1-0
  • 密码输入获得焦点 → 同时匹配前两条,取高的 input:focus0-1-1
  • 密码输入在 #myApp 内且带 required → 匹配第三条 1-2-1,无论有没有焦点都是这个值

The :is(), :not(), :has() and CSS nesting exceptions(:is():not():has() 与 CSS 嵌套的例外)

:is():not():has() 自身不算权重,但参数参与计算,取列表中最高特异性:

1
2
3
4
5
6
7
8
p { color: red; } /* 0-0-1 */
:is(p) { color: red; } /* 0-0-1 — 一样 */

h2:nth-last-of-type(n + 2) { } /* 0-1-1 */
h2:has(~ h2) { } /* 0-0-2 — has 自己不占,~ h2 占 0-0-1 */

div.outer p { } /* 0-1-2 */
div:not(.inner) p { } /* 0-1-2 — not 自己不占,.inner 占 0-1-0 */

利用这一点主动拉高特异性(但不推荐常规使用):

1
2
3
4
5
6
7
8
9
:is(p, #fakeId) { } /* #fakeId 拉高 → 1-0-0 */
h1:has(+ h2, > #fakeId) { } /* #fakeId 拉高 → 1-0-1 */
p:not(#fakeId) { } /* #fakeId 拉高 → 1-0-1 */
div:not(.inner, #fakeId) p { } /* 1-0-2 */

/* CSS 嵌套同理——行为和 :is() 一致 */
p, #fakeId {
  span { /* 1-0-1 — 取自 #fakeId + span */ }
}

甚至可以 a:not(#fakeId#fakeId#fakeID) { }3-0-1。如果用这种 hack,必须加注释说明原因。

Inline styles(内联样式)

Inline style 可视作拥有 1-0-0-0 的特异性(四栏,比任何选择器都高)。正常手段唯一的覆盖方式是 !important。很多 JS 框架和库会插入 inline style,这时可以用带 !important 的属性选择器反制:

1
2
3
p[style*="purple"] {
  color: rebeccapurple !important;
}

The !important exception(!important 例外)

技术上讲 !important 与特异性无关——它作用在级联的 Step 2(来源与重要性),而非 Step 3(特异性)。但在同一来源和层级内,!important 声明无视特异性直接胜出;如果两条同层 !important 冲突,则回到特异性比大小。

不要用 !important 来对抗特异性问题。理解并善用特异性和级联,!important 完全可以不用。如果必须用,注释清楚为什么。

正确做法:把外部样式(Bootstrap 等)用 @import layer() 导入到低优先层,而不是用 !important 硬盖。

The :where() exception(:where() 例外)

:where() 及其参数始终为 0-0-0。用途:精确选中元素但不增加任何特异性,让后续覆盖轻松。

1
2
3
4
5
/* 主题库写的——特异性为 0,开发者随便覆盖 */
:where(#defaultTheme) a { color: red; } /* 0-0-1 */

/* 开发者覆盖只需要一个 type selector */
footer a { color: blue; } /* 0-0-2 → 胜出 */

How @scope blocks affect specificity(@scope 块如何影响特异性)

@scope 规则块本身不增加特异性。但如果显式使用 :scope 伪类,它按普通伪类计算(0-1-0)。

1
2
3
@scope (.article-body) {
  :scope img { } /* :scope(0-1-0) + img(0-0-1) = 0-1-1 */
}

Tips for handling specificity headaches

Making selectors specific with and without adding specificity(精确选中,但控制权重)

同样的 DOM 位置,三种写法权重截然不同:

1
2
3
<main id="myContent">
  <h1>Text</h1>
</main>
1
2
3
#myContent h1 { color: green; } /* 1-0-1 — 最高 */
[id="myContent"] h1 { color: yellow; } /* 0-1-1 — ID 降格为属性选择器 */
:where(#myContent) h1 { color: blue; } /* 0-0-1 — 零权重,随便覆盖 */

降低 ID 特异性的技巧:用 [id="xxx"] 替代 #xxx,从 1-0-0 降到 0-1-0。或用 :where(#xxx) 直接归零。

Increasing specificity by duplicating selector(重复选择器拉高特异性)

1
2
#myId#myId#myId span { } /* 3-0-1 */
.myClass.myClass.myClass span { } /* 0-3-1 */

极少使用,如果用了务必注释。

Precedence over third-party CSS(覆盖第三方样式)

首选 @layer,而不是堆特异性。把不控制的样式导入低优先层:

1
2
3
4
@import "TW.css" layer(); /* Tailwind 被压到最低优先 */
p, p * {
  font-size: 1rem; /* 轻松覆盖,不管 TW 写了多少 class */
}

Avoiding and overriding !important(避免和覆盖 !important)

消除 !important 依赖的三个方向:

  1. 提特异性:让原本需要 !important 的声明特异性高于对手
  2. 调顺序:同等特异性下,把它放到对手后面
  3. 降对手:降低你想要覆盖的目标的特异性

如果实在逃不掉 !important(比如别人的代码你改不了),创建专门的 !important 覆写层:

1
2
3
4
5
6
7
8
/* 方法1:用 layer() 导入 */
@import "importantOverrides.css" layer();

/* 方法2:创建命名层 */
@layer importantOverrides;
@layer importantOverrides {
 /* 只放必要的 !important 覆写 */
}

放到最前面的层让 !important 优先级最高。

本文由作者按照 CC BY 4.0 进行授权