CSS层叠和继承
本文最后更新于 2025年8月2日 下午
层叠
CSS 本质上是声明规则,即在各种条件下,我们希望产生的特定的效果。如果某元素有某个类,则应用对于样式。如果 X 元素是 Y 元素的子节点,则应用那些样式。浏览器会根据这些规则,来判断每个规则应该用在哪里,并使用它们去渲染页面。
在 CSS 里实现一个效果通常有好几种方式。当 HTML 结构变化,或将同一份样式表应用到不同网页时不同的实现方式会产生不同的结果。CSS 开发很重要一点就是以可预测的方式书写规则。
首先我们需要理解浏览器如何解析样式规则。每条规则单独来看很简单,但是当两条规则提供了冲突的样式时会发生什么呢?如果你发现有一条规则没有按照预期生效,可能是因为另一条规则跟它冲突了。要想预测规则最终的效果,就需要理解 CSS 里的层叠。
层叠指的就是这一系列规则。层叠决定了如何解决冲突,是 CSS 语言的基础。当声明冲突时,层叠会依据 解析规则 来解决冲突。
解析规则
- 样式表来源:样式是从哪里来的,包括你的样式和浏览器默认样式等。
- 选择器优先级:哪些选择器比另一些选择器更重要。
- 源码顺序:样式在样式表里的声明顺序。
层叠的规则是按照这种顺序来考虑的。
样式表来源
你的样式表属于作者样式表,除此外还有用户代理样式表,即浏览器默认样式。用户代理样式表优先级低,你的样式会覆盖它们。
有些浏览器允许用户定义一个用户样式表。这是第三种来源,它的优先级介于用户代理样式表和作者样式表之间。用户样式表很少见,并且不受网站作者控制。
浏览器应用了用户代理样式后才会应用你的样式表,即作者样式表。你指定的声明会覆盖用户代理样式表里的样式。如果你在 HTML 里面链接了多个样式表,那么它们的来源都相同,即作者。
用户代理样式表因为设置了用户普遍需要的样式,所以不会做一些完全超出预期的事情。当你不喜欢默认样式时,可以在自己的样式表里设置别的值。
样式来源规则有一个例外:标记为重要的声明。在声明的后面、分号的前面加上 !important
,该声明就会被标记为重要的声明。标记了!important
的声明会被当作更高优先级的来源。因此总体的样式表来源优先级按照由高到低排列:
- 作者的
!important
- 作者样式表
- 用户代理样式表(浏览器默认样式)
理解优先级
如果无法用来源解决冲突声明,浏览器会尝试检查它们的优先级。理解优先级很重要。不理解样式的来源照样可以写 CSS,因为 99%的网站样式是来自同样的源。
浏览器将优先级分级:HTML 文档内的行内样式高于选择器样式。
行内样式
用style
属性写的样式,这个声明只会作用于当前元素。实际上行内样式属于“带作用域的”声明,它会覆盖任何来自样式表或者<style>
标签的样式。行内样式并没有选择器,因为它们直接作用于所在的元素。
为了能在样式表里覆盖行内声明,需要为声明添加!important
,这样能将它提升到一个更高优先级的来源。但是如果行内样式也被标记为!important
,则无法覆盖行内样式。最好是只在样式表内用!important
。
选择器优先级
分为内联样式和外联样式(用style
标签写的样式和引入的 CSS 文件样式)。
不同类型的选择器有不同的优先级,一个常用表示优先级的方式是用数值形式标记,具体见 选择器 。
通常最好让优先级尽可能低,这样当需要覆盖一些样式时,才能有选择空间。
源码顺序
如果两个声明的来源和优先级相同,其中一个声明在样式表中出现较晚,或位于页面较晚引入的样式表中,则该声明胜出。
选择器
选择器可以选中页面上的特定元素并为其指定样式。CSS 有各种各样的选择器。
基础选择器
标签选择器(类型选择器):匹配目标元素的标签名。
优先级标记:0,0,1类选择器:匹配 class 属性中有指定类名的元素。
优先级标记:0,1,0ID 选择器:匹配拥有指定 ID 属性的元素。
优先级标记:1,0,0通用选择器:匹配所有元素。
优先级标记:0,0,0
属性选择器
特点:
- 优先级标记:0,1,0
- 用于根据 HTML 属性匹配元素,选择器中的字母区分大小写。选择器规范 Level4 中提出了一种不区分大小写的修饰符,可以作用于任何属性选择器。用法是将 i 添加到结束方括号前面,如:input[value=”search”i]
分类:
[attr]
匹配的元素拥有指定属性 attr,无论属性值是什么,如:input[type][attr=”value”]
匹配的元素拥有指定属性 attr,且属性值是指定的字符串值,如:input[type=”radio”][attr|=”value”]
匹配的元素拥有指定属性 attr,且属性值是指定字符串值或以该字符串开头且紧跟着一个连字符。适用于语言属性,因为该属性有时候会指定一种语言的子集,比如:墨西哥西班牙语为 es-MX,普通的西班牙语为 es,如:[lang|=”es”][attr~=”value”]
“空格分隔的列表”属性选择器。匹配的元素拥有指定属性 attr,且属性值是一个由空格分隔的值列表, 列表中的某个值等于指定的字符串值,如:a[rel~=”author”][attr^=”value”]
“开头”属性选择器。匹配的元素拥有指定属性 attr,且属性值开头是指定的字符串值,
如:a[href^=”https”]。[attr$=”value”]
“结尾”属性选择器。匹配的元素拥有指定属性 attr,且属性值结尾是指定的字符串值,
如:a[href$=”.pdf”][attr*=”value”]
“包含”属性选择器。匹配的元素拥有指定属性 attr,且属性值中包含指定的字符串值,
如:[class*=”sprite-“]
更多属性选择器:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Attribute_selectors 。
伪类选择器
特点:
- 优先级标记:0,1,0
- 应用于一组 HTML 元素,却没有在 HTML 代码中用 class 属性标记这组元素
- 用于选中处于某个特定状态的元素,此状态可能是由于用户交互,也可能是由于元素相对于其父级或者兄弟元素的位置。伪类选择器始终以一个冒号(:)开始
分类:
:first-child
匹配的元素是其父元素的第一个子元素:last-child
匹配的元素是其父元素的最后一个子元素:only-child
匹配的元素是其父元素的唯一一个子元素(没有兄弟元素):nth-child(an+b)
匹配的元素在兄弟元素中间有特定的位置,公式 an+b 中 a 和 b 是整数需要赋值,公式指定要选中哪个元素。公式的工作原理是从 0 开始代入 n 的所有整数值。公式的计算结果指定了目标元素的位置:nth-last-child(an+b)
匹配的元素在兄弟元素中间有特定的位置,类似于:nth-child(),匹配的元素不是从第一个元素往后数,而是从最后一个元素开始往前数个数。括号内的公式与:nth-child() 里的公式的规则相同:first-of-type
类似于:first-child,根据拥有相同标签名的子元素中的数字顺序查找首个元素:last-of-type
类似于:last-child,根据拥有相同标签名的子元素中的数字顺序查找最后一个元素:only-of-type
类似于:only-child,根据拥有相同标签名的子元素中查找该类型的唯一一个子元素:nth-of-type(an+b)
匹配的元素在兄弟元素中间有特定的位置
类似于:nth-child,根据拥有相同标签名的子元素中的数字顺序及特定公式查找元素nth-last-of-type(an+b)
匹配的元素在兄弟元素中间有特定的位置
类似于:nth-last-child,根据拥有相同标签名的子元素中的数字顺序及特定公式查找元素,匹配的元素不是从第一个元素往后数,而是从最后一个元素开始往前数个数:not(selector)
匹配的元素不匹配括号内的选择器,括号内选择器必须是基础选择器或属性选择器,这个只能指定元素本身,无法用于排除祖先元素,同时不允许包含另一个排除选择器:empty
匹配的元素必须没有子元素
注意,如果元素包含空格就无法由该选择器匹配,因为空格在 DOM 中属于文本节点:focus
匹配通过鼠标点击、触摸屏幕或者按 Tab 键导航而获得焦点的元素:link
匹配尚未访问的链接:hover
匹配鼠标指针正悬停在其上方但没有激活的元素:active
匹配被用户激活的元素:visited
匹配已访问过的链接:root
匹配文档根元素,对 HTML 来说是<html>
元素:disabled
匹配已禁用的元素:enabled
匹配已启用的元素:checked
匹配已经针对选定的复选框、单选按钮或选择框选项:invalid
根据输入类型中的定义,匹配有非法输入值的元素(Level4):valid
根据输入类型中的定义,匹配有合法值的元素(Level4):required
匹配设置了 required 属性的元素(Level4):optional
匹配没有设置 required 属性的元素(Level4)
更多伪类选择器:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-classes 。
伪元素选择器
特点:
- 优先级标记:0,0,1
- 类似于伪类,但不匹配特定状态的元素,而是匹配在文档中没有直接对应 HTML 元素的特定部分
- 可能只匹配元素的一部分,甚至向 HTML 标记未定义的地方插入内容。伪元素选择器始终以一个双冒号(::)开始
分类:
::before
创建一个伪元素,使其成为匹配元素的首个子元素。该元素默认是行内元素,可用于插入文字图片或者其他形状。必须指定 content 属性才能让元素出现,如:.menu::before::after
创建一个伪元素,使其成为匹配元素的最后一个子元素。该元素默认是行内元素,可用于插入文字图片或者其他形状。必须指定 content 属性才能让元素出现,如:.menu::after::first-letter
用于指定匹配元素的第一个文本字符的样式::first-line
用于指定匹配元素的第一行文本的样式::selection
用于指定用户使用鼠标高亮选择的任意文本的样式。通常用于改变选中文本的 background-color。只有少数属性可以使用,包括 color、background-color、cursor、text-decoration 等::marker
用于选中一个 list item 的 marker box,后者通常含有一个项目符号或者数字。它作用在任何设置了 display: list-item 的元素或伪元素上,例如<li>
、<summary>
更多伪元素选择器:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-elements 。
组合选择器
特点:
- 将多个选择器连接组成的一个复杂选择器
- 优先级由各选择器的优先级标记累加
分类:
后代组合器( )(空格)
匹配的目标元素是其他元素的所有后代元素子代组合器(>)
匹配的目标元素是其他元素的直接后代元素嵌套组合器(&)
效果同子代组合器相邻兄弟组合器(+)
匹配的目标元素是紧跟在其元素后面的首个兄弟元素通用兄弟组合器(~)
匹配的目标元素是紧跟在其元素后面的所有兄弟元素交集组合器()(没有空格)
匹配的目标元素是符合全部选择器的元素并集组合器(,)
匹配的目标元素是所有出现的选择器的元素
总结
面对一个样式问题时,分两个步骤来解决它。首先确定哪些声明可以实现效果。其次思考可以用哪些选择器结构,然后选择最符合需求的那个。
链接样式
给链接加样式要按照一定的顺序书写选择器,因为源码顺序影响了层叠。优先级相同时,后来出现的样式会覆盖先出现的样式。如果一个元素同时处于两个或者更多状态,最后一个状态就能覆盖其他状态。如果用户将鼠标悬停在一个访问过的链接上,悬停效果会生效。如果用户在鼠标悬停时激活了链接(即点击了它),激活的样式会生效。
链接加样式顺序的口诀是“LoVe/HAte”:link(链接)、visited(访问)、hover(悬停)、active(激活)。注意,如果将一个选择器的优先级改得跟其他的选择器不一样,这个规则就会遭到破坏,可能就会带来意想不到的结果。
层叠值
浏览器遵循三个步骤,即来源、优先级、源码顺序,来解析网页上每个元素的每个属性。如果一个声明在层叠中“胜出”,它就被称作一个层叠值。元素的每个属性最多只有一个层叠值。
经验法则
在选择器中不要使用 ID。就算只用一个 ID,也会大幅提升优先级。当需要覆盖这个选择器时,通常找不到另一个有意义的 ID,于是就会复制原来的选择器,然后加上另一个类,让它区别于想要覆盖的选择器。
不要使用!important
。它比 ID 更难覆盖,一旦用了它,想要覆盖原先的声明,就需要再加上一个!important
,而且依然要处理优先级的问题。
重要提醒
当创建一个用于分发的 JavaScript 模块(如 NPM 包)时,强烈建议尽量不要在 JavaScript 里使用行内样式。如果这样做,就是在强迫使用该包的开发人员要么全盘接受包里的样式,要么给每个想修改的属性加上重要标记 !important。正确的做法是在包里包含一个样式表。如果组件需要频繁修改样式,通常最好用 JavaScript 给元素添加或者移除类。这样用户就能在使用这份样式表同时,在不引入优先级竞赛前提下,能按照自己的喜好选择编辑其中的样式。
继承
最后一种给元素添加样式的方式:继承。经常有人把层叠跟继承混淆。虽然两者相关,但应分别理解它们。如果一个元素的某个属性没有层叠值,则可能会继承某个祖先元素的值。
但不是所有的属性都能被继承。默认情况下,只有特定的一些属性能被继承,通常是我们希望被继承的那些。
文本相关
color(颜色,a 元素除外)
direction(方向)
font(字体)
font-family(字体系列)
font-size(字体大小)
font-style(用于设置斜体)
font-variant(用于设置小型大写字母)
font-weight(用于设置粗体)
letter-spacing(字母间距)
line-height(行高)
text-align(用于设置对齐方式)
text-indent(用于设置首行缩进)
text-transform(用于修改大小写)
visibility(可见性)
white-space(用于指定如何处理空格)
word-spacing(字间距)
列表相关
list-style(列表样式)
list-style-image(用于为列表指定定制的标记)
list-style-position(用于确定列表标记的位置)
list-style-type(用于设置列表的标记)
表格相关
border-collapse(用于控制表格相邻单元格的边框是否合并为单一边框)
border-spacing(用于指定表格边框之间的空隙大小)
caption-side(用于设置表格标题的位置)
empty-cells(用于设置是否显示表格中的空单元格)
印刷相关
orphans(用于设置当元素内部发生分页时在页面底部需要保留的最少行数)
page-break-inside(用于设置元素内部的分页方式)
widows(用于设置当元素内部发生分页时在页面顶部需要保留的最少行数)
特殊值
有三个特殊值可以赋给任意属性,用于控制层叠:inherit
、initial
、unset
,均为全局值。
inherit
inherit
关键字:使用继承代替一个层叠值。可以用它来覆盖另一个值,这样该元素就会继承其父元素的值。还可以使用 inherit
关键字强制继承一个通常不会被继承的属性,比如边框和内边距。
initial
initial
关键字:用于撤销某个元素的样式。每个 CSS 属性都有初始的默认值。若将initial
值赋给某个属性,那么就会有效地将该属性值重置为该属性的默认值(而非元素的初始值),这种操作相当于硬复位了该值。但注意,auto
不是所有属性的默认值,对很多属性来说甚至不是合法的值。
注意:声明display: initial
等价于display: inline
。不管哪种类型的元素都不会等于display: block
。这是因为initial
关键字是重置属性为其默认值,而非元素的初始值。inline
才是display
属性的初始值。
unset
unset
关键字:如果父元素设置对应属性值,则继承父元素属性值,如果父元素没有设置对应属性值,则是将该属性重新设置为默认值。
简写属性
简写属性是用于同时给多个属性赋值的属性。如:
font
是font-style
、font-weight
、font-size
、font-height
、font-family
的简写background
是background-color
、background-image
、background-size
、background-repeat
、background-position
、background-origin
、background-chip
、background-attachment
的简写border
是border-width
、border-style
、border-color
的简写。但这三个也是简写属性,border-width
是border-top-width
、border-bottom-width
、border-left-width
、border-right-width
的简写
简写属性可以让代码简洁明了,但是也隐藏了一些怪异行为。
覆盖样式
大多数简写属性可以省略一些值,只指定我们关注的值。但是要知道,这样做仍会设置省略的值,即它们会被隐式地设置为初始值。这会默默覆盖在其他地方定义的样式。
在所有的简写属性里, font 的问题最严重,因为它设置的属性值太多了。
简写顺序
简写属性会尽量包容指定的属性值的顺序。
但是有很多属性的值很模糊。在这种情况下,值的顺序很关键。理解这些简写属性的顺序很重要。
上右下左
当遇到像margin
、padding
这样的属性,还有为元素的四条边分别指定值的边框属性时,容易弄错这些简写属性的顺序。这些属性的值是按顺时针方向,从上边开始的。记住顺序能少犯错误。它的记忆口诀是TRouBLe:top(上)、right(右)、bottom(下)、left(左)。
这种模式下的属性值还可以缩写:
- 使用一个值,用于全部四个边
- 使用两个值,前一个值用于上下两边,后一个值用于左右两边
- 使用三个值,第一个值用于上边,第二个值用于左右两边,第三个值用于下边
- 使用四个值,按照时钟顺序依次应用于上、右、下、左四个边
水平垂直
还有一些属性只支持最多指定两个值,包括background-position
、box-shadow
、text-shadow
等(严格来讲并不是简写属性)。这些属性值顺序跟padding
这种四值属性的顺序刚好相反。比如,padding: 1em 2em
先指定了垂直方向的上/下属性值,然后才是水平方向的右/左属性值,而 background-position: 25% 75%
则先指定水平方向的右/左属性值,然后才是垂直方向的上/下属性值。
虽然看起来顺序相反的定义违背直觉,原因很简单:这两个值代表了一个笛卡儿网格。笛卡儿网格的测量值一般是按照(x,y)(水平,垂直)的顺序来的。第一个值指定了水平方向的偏移量,第二个值指定了垂直方向的偏移量。
如果属性需要指定从一个点出发的两个方向的值,就想想“笛卡儿网格”。如果属性需要指定一个元素四个方向的值,就想想“时钟”。