CSS 层叠
CSS 层叠决定多条声明同时作用于同一个元素、同一个属性时,最终采用哪一个值。选择器优先级只是其中一步:一条声明可能因为媒体条件不成立而没有参与比较,也可能在比较选择器之前,就因来源、重要性或层叠层而落后。
排查“样式为什么没生效”,要先找到目标元素上的候选声明,再沿层叠顺序判断。继承则是在元素需要从父元素取得值时发挥作用,不能把父元素的高优先级选择器直接算到子元素身上。
1. 面试核心
先记住这几条,再读正文- 层叠比较的是同一元素、同一属性上的候选声明。先确认规则有效、选择器匹配和条件成立,再比较来源与重要性等顺序。卡 · 声明为什么会竞争
- 选择器优先级可以表示为 ID、类/属性/伪类、类型/伪元素的三元组,按位从左到右比较;不是能向高位进位的十进制总分。卡 · 选择器表达的关系
- 伪类匹配状态或关系,伪元素描述元素的特定部分或生成的盒子。
:hover与::before的职责及优先级不同。卡 · 选择器表达的关系 - 同一来源中的层叠层先于选择器比较:普通声明后声明的层优先,重要声明顺序反转;未分层的普通声明又高于分层普通声明。卡 · 层叠层
!important改变声明所属的层叠级别,不增加选择器的三元组分值。行内普通样式也能被重要声明覆盖,但不能把行内样式简化成“任意 1000 分”。卡 · 重要性先于选择器:where()的优先级为 0;:is()、:not()、:has()采用参数中最高的优先级,不能把这些函数都当作普通伪类计数。卡 · 函数伪类- 直接作用于子元素的声明与父元素上的声明不参与同一场比较。
color等可继承属性可从父元素取得值,但父元素的!important不会迫使子元素继承。卡 · 继承的对象是值 initial、inherit、unset、revert、revert-layer回退的层次不同;不能都理解为“恢复浏览器默认”。卡 · 几个回退关键字- 普通自定义属性参与层叠并默认继承。
var()的回退用于变量缺失等情况,不保证变量替换后一定符合目标属性的语法。卡 · 自定义属性与计算值
2. 声明为什么会竞争
声明是一个属性和值,例如 color: red。一个元素可以同时匹配多个选择器,每条规则又可能为同一属性提供不同值,层叠需要从中决定胜者。
<style>
.message { color: blue; }
#notice { color: red; }
.message { color: green; }
</style>
<p id="notice" class="message">申请已提交</p>
这里三个 color 都有效,也都直接匹配这段文字。它们来自同一作者样式表,没有分层且都不是重要声明,才需要比较选择器优先级。#notice 胜出,因此文字为红色;后写的 .message 不会仅因出现得晚就覆盖 ID 选择器。
来源区分浏览器样式、用户样式与作者样式。作者样式包括站点样式表和元素上的 style,用户样式则用于用户对页面的定制,不等同于页面 JavaScript 动态插入的样式表。
在普通文档中,可以按以下顺序排查:
- 属性和值是否有效,选择器是否匹配,
@media、@supports等条件是否成立。 - 声明的来源与重要性属于哪一层级。
- 同一来源内是否存在层叠层或行内声明等更早的排序条件。
- 条件相同时,再比较选择器优先级;使用
@scope时还要检查作用域接近程度。 - 仍然相同时,后出现的声明胜出。
Shadow DOM 还有封装上下文的排序规则,不能把普通文档中的简化步骤当作跨所有树边界的完整算法。工程中先看 DevTools 展示的具体规则及来源,再查对应条件。
3. 选择器与优先级
3.1 选择器表达的关系
选择器描述一组匹配条件。类、属性、状态和元素关系可以组合,而不是每条规则都必须从某个 ID 开始。
| 类别 | 示例 | 含义 |
|---|---|---|
| 类型、通用 | button、* | 按元素类型匹配,或匹配任意元素 |
| 类、ID | .primary、#submit | 匹配 class 中的标记或指定 id |
| 属性 | [disabled]、[type="email"] | 匹配属性存在或属性值 |
| 后代、子元素 | .form button、.form > button | 任意层级后代,或直接子元素 |
| 相邻、后续兄弟 | h2 + p、h2 ~ p | 紧接其后的兄弟,或其后满足条件的兄弟 |
| 状态、结构伪类 | :focus-visible、:checked、:first-child | 匹配状态或结构条件 |
| 伪元素 | ::before、::after、::first-line | 描述特定部分或生成内容 |
伪类在匹配条件上补充状态或关系,伪元素用于描述元素的特定部分,不是 JavaScript 中新增的普通 DOM 子节点。
<style>
.required::before { content: "必填 "; color: #b91c1c; }
input:focus-visible { outline: 2px solid #2563eb; }
</style>
<label class="required" for="name">姓名</label>
<input id="name" required>
标签前会生成提示文字,输入框获得适当的键盘焦点时显示轮廓。这里 ::before 不会变成可通过 querySelector 选中的普通 DOM 元素;可以用 getComputedStyle(element, '::before') 检查其样式。重要说明仍应作为可靠的页面内容或控件关系表达,不能仅依赖装饰性的生成内容。
3.2 三元组的比较
选择器优先级(specificity)通常写成 (A, B, C):A 统计 ID,B 统计类、属性和普通伪类,C 统计类型和伪元素。通用选择器与组合关系符不增加这三个数。
| 选择器 | 优先级 |
|---|---|
* | (0, 0, 0) |
button | (0, 0, 1) |
.form button | (0, 1, 1) |
input[type="email"]:focus | (0, 2, 1) |
.required::before | (0, 1, 1) |
#app .form button | (1, 1, 1) |
先比 A,A 相同再比 B,最后比 C。再多的类也不会向 ID 那一位“进位”,所以不应把它们解释为 ID 固定 100 分、类固定 10 分后相加。
逗号分隔的选择器列表也不是把所有分支相加。普通列表中,元素匹配的分支分别有自己的优先级;不能因为旁边列了一个不匹配的 ID,就认为另一条类选择器也获得了 ID 权重。下一节的函数参数规则与它不同。
3.3 函数伪类
:where() 可以表达复杂匹配条件,但自身及参数的优先级都计为 0,适合需要被业务样式轻松覆盖的基础规则。
<style>
:where(#panel .title) { color: red; }
.title { color: blue; }
</style>
<div id="panel"><h2 class="title">申请记录</h2></div>
虽然 :where() 参数中包含 ID,文字仍是蓝色,因为整段 :where(...) 的优先级是 (0, 0, 0),.title 是 (0, 1, 0)。
:is()、:not()、:has() 不额外增加一个普通伪类权重,而是取参数中最高的优先级。
<style>
:is(.title, #unused) { color: red; }
.title { color: blue; }
</style>
<h2 class="title">申请记录</h2>
这里文字是红色。#unused 并未匹配当前元素,但它仍让这段 :is() 具有 ID 级别的优先级。把前一条改成普通列表 .title, #unused,当前元素匹配的 .title 分支只有类权重,后写的 .title 就可以胜出。
:nth-child(... of S) 还要将自身的伪类权重与 S 中最高的权重相加。例如 :nth-child(2 of .item) 是 (0, 2, 0)。不熟悉的函数选择器应按它自己的规则判断,不能见到冒号就统一加 1。
4. 来源、重要性与层叠层
4.1 重要性先于选择器
重要声明是在值后标记 !important 的声明。它进入不同的层叠级别,选择器本身的三元组没有改变。
下面按从高到低列出来源与重要性的主要顺序:
| 优先顺序 | 声明或值的来源 |
|---|---|
| 最高 | 正在生效的 CSS 过渡值 |
| 其后 | 浏览器重要声明 |
| 其后 | 用户重要声明 |
| 其后 | 作者重要声明 |
| 其后 | CSS 关键帧动画值 |
| 其后 | 作者普通声明 |
| 其后 | 用户普通声明 |
| 最后 | 浏览器普通声明 |
重要声明让用户和浏览器有机会坚持必要的呈现要求,所以来源顺序与普通声明不同。过渡期间的中间值属于另外的层级;不能据此说永久改动了目标声明的优先级。
<style>
#notice { color: red; }
.message { color: blue !important; }
</style>
<p id="notice" class="message" style="color: green">申请已提交</p>
文字为蓝色。类选择器的重要声明高于 ID 普通声明,也高于行内普通声明。若行内声明改成 color: green !important,在作者重要声明范围内,行内声明又高于样式表中的重要声明。
把行内样式记成四元组的第一位,可以帮助记忆局部比较,但它不是一个可以被任意多个 ID 选择器累加超过的数值。更准确的解释是:行内声明有独立的层叠排序位置,同时仍受来源与重要性约束。
4.2 层叠层
层叠层(cascade layer)用 @layer 为一组声明建立排序层次。它适合先安排基础样式、组件样式与业务覆盖之间的关系,再在层内部使用简单选择器。
<style>
@layer base, components;
@layer base { #notice { color: red; } }
@layer components { .message { color: blue; } }
</style>
<p id="notice" class="message">申请已提交</p>
文字是蓝色。普通声明中,后声明的 components 层高于 base,因此比较层顺序后就得到结果,无须让 .message 与 #notice 比优先级。
同一来源内,可以这样记住层顺序:
| 声明类别 | 较高优先级 |
|---|---|
| 分层普通声明 | 后声明的层 |
| 普通声明,分层与未分层相比 | 未分层声明 |
| 分层重要声明 | 先声明的层 |
| 重要声明,分层与未分层相比 | 分层声明 |
<style>
@layer base, components;
@layer base { .message { color: red !important; } }
@layer components { #notice { color: blue !important; } }
#notice { color: green !important; }
</style>
<p id="notice" class="message">申请已提交</p>
这一次文字是红色。重要声明的层顺序反转,base 高于 components,也高于未分层的重要声明。若元素上另有作者行内重要声明,它仍高于这些样式表层。
层的顺序在首次建立时确定;后续再次向已存在的层添加规则,不会因此把整层移动到最后。层内仍可能发生选择器和源码顺序的比较。引入层叠层后,不能继续把“把覆盖文件放在最后”当作唯一的覆盖策略。
4.3 顺序与作用域
在来源、重要性、层、选择器等前置条件相同时,源码中后出现的声明胜出。外部样式表的网络下载完成顺序不会把文档中的样式表顺序改写;动态改变插入位置才可能改变相应顺序。
项目使用 @scope 时,相同优先级下还会比较元素到作用域根的距离,较近的作用域优先于较远的作用域,然后才比较源码顺序。作用域不是给选择器凭空加一个 ID 权重。校招基础回答先掌握普通文档和层叠层;明确遇到作用域或 Shadow DOM 时,再按这些额外条件继续判断。
5. 继承与回退
5.1 继承的对象是值
继承让属性从父元素取得相应值。color、font-family 等通常可继承;margin、padding、border 等通常不继承。具体是否继承由每个属性的定义决定。
<style>
#parent { color: red !important; margin: 30px; }
.child { color: blue; }
</style>
<div id="parent"><span class="child">子元素</span><span class="plain">普通文字</span></div>
.child 是蓝色,.plain 继承红色。父元素的 !important 只影响父元素自己的层叠,不会强迫 .child 忽略直接声明。两者的 margin 也不会因为父元素设了 30px 就都继承 30px。
有些效果看起来“传给了孩子”,却不是属性继承。例如父元素整体设置 opacity 会影响整组内容的最终呈现,但不能据此说 opacity 的计算值被每个子元素继承。
5.2 几个回退关键字
| 关键字 | 对当前属性的含义 |
|---|---|
inherit | 显式使用继承值,即使该属性通常不继承 |
initial | 使用规范定义的初始值,不等于浏览器给该元素写的默认样式 |
unset | 可继承属性按 inherit,不可继承属性按 initial |
revert | 撤回当前来源的影响,按相应的来源回退规则重新求值 |
revert-layer | 撤回当前层的影响,回到层叠中较低优先级的层或来源 |
以 display 为例,属性的初始值是 inline,但浏览器默认样式会把普通 div 设成块级显示。因此 display: initial 与撤回作者样式后的显示结果可能不同。
<style>
div { display: flex; }
.initial { display: initial; }
.reverted { display: revert; }
@layer base, overrides;
@layer base { .message { color: blue; } }
@layer overrides { .message { color: revert-layer; } }
</style>
<div class="initial">初始值</div>
<div class="reverted">撤回作者来源</div>
<p class="message">撤回当前层</p>
在普通浏览器默认样式下,.initial 是行内显示,.reverted 回到 div 的块级显示,.message 采用 base 层的蓝色。revert 没有简单选择同一作者来源里更早的 display: flex;revert-layer 也不是把值一律重置成初始值。
6. 自定义属性与计算值
自定义属性是以 -- 开头、可以通过 var() 使用的属性。普通未注册的自定义属性参与层叠,默认继承,所以适合让一个区域及其后代共享颜色、间距等值。
<style>
.theme { --accent: #2563eb; }
.message { color: var(--accent, red); }
.local { --accent: #15803d; }
</style>
<div class="theme">
<p class="message">区域颜色</p>
<p class="message local">局部颜色</p>
</div>
第一段继承 .theme 的 --accent,第二段在自身覆盖该变量。var(--accent, red) 的回退值用于 --accent 缺失或处于相应无效状态时,并不会替每一种目标属性验证变量内容。
<style>
.parent { color: green; }
.message { color: blue; color: var(--bad, red); --bad: 20px; }
</style>
<div class="parent"><p class="message">变量替换后的结果</p></div>
--bad 已存在,因此不会选择回退的红色;替换得到的 color: 20px 却不符合颜色语法。这是在计算值阶段才失效,不能返回去重新选中前面的 color: blue。这里 color 是可继承属性,最终使用父元素的绿色。
这与直接写一条语法无效的 color: 20px 不同:直接无效的声明在更早阶段就会被丢弃,前面的有效声明仍可能胜出。排查变量问题时,要同时检查变量来自哪里、替换后是否符合目标属性的要求。
7 · 面试题卡片
本页题目与参考回答
7.1 CSS 声明冲突时,是否直接比较选择器优先级?
考察重点:候选声明、来源与重要性、后续排序。
参考回答
不能直接比较,先确认规则参与层叠,再比较来源、重要性及层等更早的排序条件。 规则无效、选择器不匹配或媒体条件不成立,就没有这次竞争。前置条件相同后才比较选择器优先级,使用作用域时还需考虑接近程度,最终才由源码顺序打破平局。继承值也不是拿父元素选择器加入子元素的同一场比较。
7.2 CSS 选择器有哪些,优先级如何计算?
考察重点:匹配关系、三元组与比较前提。
参考回答
选择器可以按类型、类、ID、属性、状态和元素关系匹配;优先级通常按 ID、类/属性/伪类、类型/伪元素三个位置比较。 从左到右比,前一位不同就决定结果,不做十进制进位。通用选择器与组合关系符不增加计数。只有来源、重要性、层等前置条件相同时,这个比较才决定胜者;普通逗号列表也不把所有分支相加。
7.3 伪类和伪元素有什么区别?
考察重点:状态匹配、元素部分与生成内容。
相关内容:选择器表达的关系。
参考回答
伪类补充状态或结构条件,伪元素描述元素的特定部分或生成内容。 例如 :focus-visible 匹配焦点呈现条件,::before 可生成前置内容。普通伪类计入三元组的类一栏,伪元素计入类型一栏;函数伪类另有特定规则。伪元素不是普通 DOM 子节点,不能把其生成内容等同于通过 JavaScript 插入了一个真实元素。
7.4 层叠层为什么能让低优先级选择器覆盖 ID 选择器?
考察重点:层顺序与普通、重要声明的反转。
相关内容:层叠层。
参考回答
同一来源内先比较层,只有同层等条件下才继续比较选择器。 普通声明中后声明的层优先,未分层普通声明又高于分层普通声明;重要声明则是先声明的层优先,分层重要声明高于未分层重要声明。行内声明还有独立排序位置。层顺序在首次建立时确定,后续给层追加规则不会把层移到最后。
7.5 !important、ID 选择器和行内样式怎样比较?
考察重点:重要性与选择器权重的区别。
相关内容:重要性先于选择器。
参考回答
!important 改变层叠级别,不增加选择器的三元组分数。 在作者样式范围内,类选择器的重要声明可以覆盖 ID 普通声明和行内普通声明;行内重要声明又高于作者样式表的重要声明。继续跨来源比较时,还存在用户、浏览器重要声明,以及过渡值等层级,所以不能只背“行内大于 ID 大于类”的一条顺序。
7.6 :where() 与 :is() 的优先级为什么不同?
考察重点:函数伪类的参数规则。
相关内容:函数伪类。
参考回答
:where() 连同参数的优先级为 0,:is() 使用参数中最高的优先级。 所以 :where(#panel .title) 可以被普通 .title 覆盖,而 :is(.title, #unused) 即使通过 .title 匹配,仍具有 ID 级别的优先级。:not()、:has() 也采用参数最高值;普通逗号列表则按匹配分支分别处理。
7.7 哪些 CSS 属性会继承,父元素的 important 会强迫子元素继承吗?
考察重点:属性继承性、值与声明竞争范围。
相关内容:继承的对象是值。
参考回答
是否继承由属性定义决定,父元素的 !important 不会强迫子元素继承。 color、font-family 等通常可继承,margin、padding、border 等通常不继承。子元素有自己的 color 声明时,在子元素上决定值;没有相关声明时才可能从父元素继承。整体透明度影响后代呈现,也不能据此判断 opacity 本身会继承。
7.8 initial、unset、revert 和 revert-layer 有什么区别?
考察重点:属性初始值、继承、来源与层的回退。
相关内容:几个回退关键字。
参考回答
initial 回到规范初始值,unset 按属性是否继承选择继承或初始值,revert 撤回当前来源,revert-layer 撤回当前层。 例如 display 的初始值是 inline,但浏览器会把普通 div 设为块级,所以 display: initial 不等于恢复默认 div 显示。revert-layer 可以让较低优先级层中的业务基础声明重新生效,而不是一律回到初始值。
7.9 var() 提供了回退值,变量内容不符合属性语法时为何仍不使用回退?
考察重点:变量存在性、替换阶段与计算值失效。
相关内容:自定义属性与计算值。
参考回答
回退值主要处理变量缺失等状态,不是对替换后的目标属性进行通用语法兜底。 --bad: 20px 存在时,color: var(--bad, red) 会先替换成 color: 20px,随后在计算值阶段失效;不会改用红色,也不会重新挑选前面的蓝色声明。示例中的 color 按继承规则使用父元素绿色。直接写语法错误的声明则更早被丢弃,处理阶段不同。
8. 参考资料
- CSS Cascading and Inheritance Level 5:层叠、继承与层
- Selectors Level 4:选择器优先级
- CSS Cascading and Inheritance Level 6:作用域
- CSS Custom Properties:自定义属性与计算值
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明