开始根据林想的Web工坊大佬的建议路线学习CSS。
本文涉及内容会如下:
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两波,分别进行排序,会先忽略层的概念,级联顺序如下(从低到高):
| 优先级 | 来源 | 重要性 |
|---|---|---|
| 1 | user-agent(浏览器) | normal |
| 2 | user(用户) | normal |
| 3 | author(开发者) | normal |
| 4 | CSS keyframe animations | — |
| 5 | author(开发者) | !important |
| 6 | user(用户) | !important |
| 7 | user-agent(浏览器) | !important |
| 8 | CSS 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 中的两条未分层声明:0 和 3px。
Step 3 Specificity:两条选择器都是 li,特异性相同。
Step 4 Scoping proximity:都不在 @scope 内,不适用。
Step 5 Order of appearance:3px 后声明,胜出。
最终结果: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 样式 | 重要性 |
|---|---|---|
| 1 | A — 第一个层 | normal |
| 2 | B — 第二个层 | normal |
| 3 | C — 最后一个层 | normal |
| 4 | 所有未分层样式 | normal |
| 5 | inline style | normal |
| 6 | animations | — |
| 7 | 未分层样式 | !important |
| 8 | C — 最后一个层 | !important |
| 9 | B — 第二个层 | !important |
| 10 | A — 第一个层 | !important |
| 11 | inline style | !important |
| 12 | transitions | — |
关键规律:
- 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:
- Important user style
- Important user-agent style
- 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,完整的级联优先级如下(从低到高):
| 优先级 | 样式来源 | 重要性 |
|---|---|---|
| 1 | user-agent — 第一个声明的层 | normal |
| user-agent — 最后一个声明的层 | normal | |
| user-agent — 未分层样式 | normal | |
| 2 | user — 第一个声明的层 | normal |
| user — 最后一个声明的层 | normal | |
| user — 未分层样式 | normal | |
| 3 | author — 第一个声明的层 | normal |
| author — 最后一个声明的层 | normal | |
| author — 未分层样式 | normal | |
| inline style | normal | |
| 4 | animations | — |
| 5 | author — 未分层样式 | !important |
| author — 最后一个声明的层 | !important | |
| author — 第一个声明的层 | !important | |
| inline style | !important | |
| 6 | user — 未分层样式 | !important |
| user — 最后一个声明的层 | !important | |
| user — 第一个声明的层 | !important | |
| 7 | user-agent — 未分层样式 | !important |
| user-agent — 最后一个声明的层 | !important | |
| user-agent — 第一个声明的层 | !important | |
| 8 | transitions | — |
!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-color 和 color 的动画被完全忽略,不会被混合。
@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: unset 或 all: revert 就搞定了,不需要逐属性重置。
Inheritance
CSS 属性分为两类:可继承的 和 不可继承的。当一个元素没有对某个属性显式设值时,浏览器根据属性类型决定从哪里取值。
Inherited properties(可继承属性)
没有显式设值时,元素从父元素获取该属性的计算值。只有文档根元素(<html>)才会获取属性的初始值(CSS 规范定义的默认值)。
常见可继承属性:color、font-family、font-size、line-height、text-align、visibility、cursor 等,大多是文字和排版相关。
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 规范定义的初始值,不会去父元素找。
常见不可继承属性:border、margin、padding、width、height、background、position、display 等,大多是盒模型和布局相关。
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;
}
可继承不可继承简单对比
| 类型 | 没设值时从哪取 | 常见属性 |
|---|---|---|
| 可继承 | 父元素的计算值 | color、font-*、text-*、line-height、visibility |
| 不可继承 | CSS 规范的初始值 | border、margin、padding、width、display |
文字排版相关属性大多可继承(从父拿),盒模型/布局属性大多不可继承(从规范初始值拿)。inherit 可以打破这个规则,all 可以一键重置。
Specificity
特异性是赋予每条 CSS 声明的权重分数。当多条声明为同一元素设置同一属性时,匹配该元素的选择器中权重最高者的声明生效。
How is specificity calculated?
特异性是一个三栏的值:ID - CLASS - TYPE,从左到右逐栏比较。
Selector weight categories(选择器权重类别)
| 权重栏 | 包含的选择器 | 每个计为 |
|---|---|---|
| ID | ID selector(如 #example) | 1-0-0 |
| CLASS | class 选择器(.myClass)、属性选择器([type="radio"])、伪类(:hover、:nth-of-type(3n)、:required) | 0-1-0 |
| TYPE | type 选择器(p、h1)、伪元素(::before、::placeholder) | 0-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:focus即0-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 依赖的三个方向:
- 提特异性:让原本需要
!important的声明特异性高于对手 - 调顺序:同等特异性下,把它放到对手后面
- 降对手:降低你想要覆盖的目标的特异性
如果实在逃不掉 !important(比如别人的代码你改不了),创建专门的 !important 覆写层:
1
2
3
4
5
6
7
8
/* 方法1:用 layer() 导入 */
@import "importantOverrides.css" layer();
/* 方法2:创建命名层 */
@layer importantOverrides;
@layer importantOverrides {
/* 只放必要的 !important 覆写 */
}
放到最前面的层让 !important 优先级最高。