上周在做一个H5详情页,body里的font-family和color都写好了,页面正文文字全部正常,唯独底部那个“提交订单”按钮,字体还是浏览器默认的宋体,字号明显小一圈。我第一反应是某个框架样式的优先级太高,在控制台翻了半天也没找到覆盖规则。最后点开计算样式一看,才发现问题压根不在优先级上,而在CSS最容易被低估的一个机制——继承关系:表单控件默认不继承body上设置的字体属性。
说白了,CSS继承关系不像层叠优先级那样经常被人挂在嘴边,但它每天都在偷偷决定文字的字体、颜色、行高、列表序号,甚至按钮长什么样。如果你从来没系统梳理过它,遇到“明明写了全局样式,子元素却不生效”这类问题,排起错来很容易原地打转。
这篇笔记就把继承关系从头到尾过一遍。先讲清楚继承机制到底怎么运作,再给一份能直接对照的“可继承/不可继承”清单,然后是inherit、initial、unset、revert这四个关键字的实战区分,最后落到H5开发里最常见的继承坑和DevTools排查方法。想快速解决问题的可以直接跳到第四节,想彻底弄明白来龙去脉的,建议按顺序看。
1. CSS继承的本质:先从一个反直觉的bug说起
1.1 按钮字体不对,问题出在“表单控件默认不继承字体”
先还原一下开头那个案例。页面里只有几行简单的代码:
<style> body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; color: #333; } </style> <button>提交订单</button>在绝大多数浏览器里,这个按钮的字体会是系统默认的按钮字体,颜色也不是body里设置的#333,而是接近黑色的“ButtonText”。第一眼看上去像是继承失效了,但准确说不是“失效”,而是浏览器默认样式表(UA样式表)抢在了继承前面。
每个浏览器都有一份内建的默认CSS,比如Chrome里button本身就有一段这样的默认规则:
button { font: -webkit-small-control; color: ButtonText; }它属于用户代理样式,优先级很低,但无论如何它是“一条显式规则”。继承的机制是:只有当一个元素在某个属性上没有拿到任何显式声明时,才会去父元素取继承值。既然button的默认样式已经给font和color都写了值,继承值就没有机会上位,body上的font-family自然传不进来。
解法也很简单,让按钮显式声明“我跟随父级”:
button, input, select, textarea { font: inherit; color: inherit; }加完之后,按钮的字体和正文统一了,颜色也统一了。很多reset和normalize.css里都有这么一行,原因就在这里。
1.2 继承到底是怎么运作的:沿着DOM树向上取“计算值”
CSS里的继承,指某个属性的值会从父元素自动传递给后代元素,后代不需要写任何额外样式就能用上。它的运作方向是“从外到内、从父到子、始终朝下”的,跟CSS选择器匹配完全是两回事。
判断规则只有一条:当后代元素在某个属性上没有可用的声明值时,浏览器就往上找一个“最近的祖先元素”,把这个祖先上对应属性的计算值拿过来用。如果最近的父元素因为某些原因没有显式值,就继续往上找,直到根元素;如果根元素也没有,那就用该属性的初始值兜底。
这里特别容易忽略一个词:计算值。继承传下去的不是你源码里写的那一串字符,而是浏览器已经算好的最终数值。举个很典型的例子:
<div style="font-size: 2em"> <p>这段文字继承了什么值?</p> </div>如果根元素是16px,那么div的计算值是32px,p继承到的就是“32px”这个具体长度,而不是“2em”这个写法。之后p自己再设置font-size时,它的计算基准就是这32px。
这个特性对em尤其致命。em是按父元素字号计算的相对单位,如果父元素自己也用em,就会一层一层叠乘,产生“越往下文字越大”的效果。后面第四节我会专门展开这个坑。
1.3 为什么需要继承机制,它和层叠是什么关系
继承最大的价值是减少重复声明。没有继承的话,你必须在每个p、span、li上重新写一遍font-family和color,改一次配色就是全站翻个底朝天。有了继承,body上写一次,整棵DOM树默认跟随,这就是“全局样式”的底层原理。
但要特别留意继承与层叠的关系。CSS层叠负责在“所有命中的显式声明”里按优先级评出一个胜者;继承只是层叠之后没人胜出时的兜底方案。换句话讲,继承在整条裁决链条里是排在最后面的,它连一条通配符规则都打不过。
看这个例子:
<style> div { color: red; } * { color: blue; } </style> <div> <p>这段文字是什么颜色?</p> </div>很多人会猜红色,因为p没有显式设置color,父div又是red,所以“应该继承红色”。但实际上p是蓝色。因为*选择器虽然特异性是0,但它是一条实打实的显式规则,优先级依然高于继承值。
这类案例在带reset的工程里经常踩到。一旦理解了“继承值永远排在所有显式声明之后”,你就会明白为什么暴力reset会带来那么多匪夷所思的颜色、字体问题。
2. 一张表分清“能继承”和“不能继承”
2.1 默认会继承的属性:字体、文本、列表与表格类
不是所有CSS属性都具备可继承性,这是属性定义里写死的。我平时记忆它们时会分三堆:字体类、文本类、杂项类。
| 分类 | 可继承属性 |
|---|---|
| 字体类 | font-family、font-size、font-weight、font-style、font-variant、font-stretch、line-height、letter-spacing |
| 文本类 | color、text-align、text-indent、text-transform、word-spacing、white-space、direction |
| 杂项类 | visibility、cursor、list-style(含list-style-type等)、quotes、border-collapse、border-spacing、caption-side、empty-cells |
说几个实际项目里容易感知到的:
- color:父元素设置文字颜色,后代没有显式color时都会跟随。
- font-size:父元素设置字号,后代文字默认跟着缩放。
- line-height:如果父元素写了line-height: 1.5,后代默认也是1.5倍行高。
- cursor:父容器加了cursor: pointer,里面所有子元素都是手型,想改成默认还得自己重置。
- list-style:在ul上设置list-style: none,内部的li会继承这个设置,列表序号消失。
这些属性基本都是“描述文字与外观的细节”,不掺和布局尺寸,所以向下传递很安全。
顺带说一句,line-height也是一个隐藏很深的天坑:如果父元素写的是line-height: 1.5这种不带单位的数字,子元素继承的是“倍率1.5”,等它自己字号变化时行高会同步缩放;但如果写的是line-height: 16px,子元素继承的是固定px,字号一旦变大,行高就会显得拥挤。所以做组件库时,全局行高更推荐用无单位数字。
2.2 默认不继承的属性:盒模型、布局、背景与特效
不继承的属性比能继承的更庞大,大致可以分为四类:
- 盒模型:width、height、margin、padding、border,以及min-width、max-width等。
- 布局定位:display、position、top、right、bottom、left、float、clear、flex、grid相关属性。
- 背景与绘制:background全家桶、box-shadow、opacity。
- 动效与状态:transform、transition、animation、clip、overflow、z-index、outline。
这类属性不继承是有道理的。如果margin可以继承,那子元素会全部叠加父级的外边距,整个页面间距直接乱套;如果background可以继承,那么页面底部每个层级都会重新绘制一遍背景图,性能和视觉双重灾难。这些属性跟当前元素的独立盒子强相关,理应各算各的。
还有一个容易混淆的点:box-sizing不继承。很多人会在父元素写box-sizing: border-box,然后发现子元素还是content-box,原因就是它不具备可继承性。工程里通常靠通配符统一:
*, *::before, *::after { box-sizing: border-box; }这种方法本质上不是继承,而是用“命中所有元素的显式规则”来模拟全局统一效果。
2.3 “看起来像继承”的特殊属性:text-decoration 不是继承
有些属性在设计上不允许继承,但视觉表现又像继承了。最典型的就是text-decoration。它的Inherited标记是no,但父元素给文字画了下划线,子元素的文字视觉上也会有下划线,往下嵌套,下划线一路穿到底。
真正的机制是:text-decoration会跨越多段内联内容,绘制到后代文本上。它不是“把属性值传下去了”,而是“绘制效果沿文本流延伸了”。这意味着子元素自己即使什么样式都没写,也会被外层的装饰线牵连;想单独摘掉某段文字的下划线,不能靠默认值清空,要显式写:
.child { text-decoration: none; }类似的视觉假象还有outline、box-shadow、opacity引起的层叠关系变化。但它们跟text-decoration还不太一样,outline和box-shadow根本不参与子元素的任何渲染,只是父级绘制到子元素上层;opacity一旦不是1,整个子树会被合成到同一个绘制层里,视觉上像是“祖先的透明度影响了后代”,但这不是属性继承,而是渲染层的物理表现。
判断一个属性到底是否可继承,最稳妥的办法是查MDN每个属性页面上的Inherited字段。我这些年记下来的结论也有例外变动,所以不依赖记忆,需要精确时直接查文档。
3. 继承与层叠的四个关键字:inherit、initial、unset、revert
3.1 四个关键字到底改变了什么
当默认继承行为满足不了需求时,CSS还提供了继承相关的显式关键字:inherit、initial、unset、revert。
| 关键字 | 行为 | 典型场景 |
|---|---|---|
| inherit | 强制取父元素的计算值 | 按钮/输入框恢复字体和颜色、链接跟随正文颜色 |
| initial | 取属性的初始定义值,而不是浏览器默认样式 | color: initial回到黑色,display: initial回到inline |
| unset | 对可继承属性等同inherit,对不可继承属性等同initial | 组件reset时一条声明打散多个属性 |
| revert | 回到浏览器默认样式(UA样式或用户样式) | 恢复某个元素在浏览器里的“原生长相” |
我用一个非常容易踩的例子说明initial和inherit的差别。假设父容器文字是红色,某个子按钮想跟随红色,那就应该写color: inherit。如果你误写成color: initial,按钮文字会变成属性本身的初始值——其实就是黑色。因为color的初始值被定义为canvastext,大概就是接近黑色,它跟“父级颜色”没有半毛钱关系。
再比如display。display: initial的结果是inline,因为display属性的初始定义就是inline,不是父元素的display,也不是block。每天都有新手在按钮上写display: initial想恢复成块级,结果越搞越迷糊。
unset在reset里更实用。比如你想让某个组件“从头开始搭样式”,可以写:
.reset-card { all: unset; }all: unset会对所有属性生效。可继承的属性回到继承状态,非继承的属性回到初始化状态。但它很暴力,一般只在调试第三方组件、或做真正意义上从零开始的原子组件时才建议用。
revert最典型的使用场景是“我想让浏览器原始默认恢复”,而不是“回到属性初始值”。比如button被框架样式改得一塌糊涂,你希望先还原成系统按钮再微调,就可以:
button.fix { all: revert; }revert和initial的差别就在这里:initial回到的是CSS规范里这个属性的初始值,而revert回到的是浏览器默认样式表对当前元素设置的规则。对button来说,后者才是你潜意识里那个“系统原装按钮”。
3.2 优先级判定:继承值永远排最后
说完关键字,再把层叠的裁决顺序完整过一遍。浏览器确定一个元素某个属性的最终值时,大致会走这几步:
- 收集所有命中该元素的规则(包括作者样式、浏览器默认样式、用户样式)。
- 按重要程度排序:标注了!important的排在普通的上面。
- 普通声明里,按选择器特异性比较:行内样式 > ID > 类/属性/伪类 > 元素/伪元素。
- 特异性相同时,后出现、后加载的规则生效。
- 如果以上所有来源都没有命中,才使用继承值。
- 连继承都没有,就使用属性初始值。
这里要再次强调:继承值是整个链条的最后兜底,就算是一条特异性为0的* { color: blue },也能碾压从父级传过来的color: red。所以不要以为“父子关系越近,优先级越高”,继承关系里没有远近高低之分,只有“有没有被显式声明拦截”的区别。
但有一个细节要注意:子元素的继承值取的是父元素的计算值。如果父元素自己也靠继承拿到值,子元素拿到的就是这个继承链条一节一节传导下来的最终结果。所以你可以说继承是“沿着最近的有效祖先一路递下来”,但判断时只需要看最终父元素的计算值就够了,不需要逐层分析“哪条规则先声明”。
3.3 自定义属性(CSS变量)其实也是继承的
除了传统属性,CSS自定义属性(也就是常说的CSS变量)同样参与继承。很多人用var()半天,没意识到自己一直在吃继承关系的红利。
比如你在一张卡片容器上定义一个变量:
.card { --btn-bg: #ff6b35; } .card button { background: var(--btn-bg); }因为自定义属性会从.card向下继承,所以.card内部任意后代button都能取到这个变量;但.card外面的兄弟元素读不到。
这个特性在主题切换场景里价值巨大。我们可以把界面里所有主题相关值都做成变量,然后在某个容器根上统一切换:
document.querySelector('.card').style.setProperty('--btn-bg', '#1677ff');一旦在容器上改了变量值,它的所有后代通过var()引用到的颜色、间距、字号会同步更新。整个过程没有一条条改样式的成本,批量换肤靠的正是自定义属性的继承传递能力。
需要注意的是,CSS变量本身不区分属性类别,所有自定义属性默认都是可继承的。变量名大小写敏感,--BtnBg和--btn-bg是两个完全不同的东西;同名变量在嵌套结构里按就近原则解析,这一点跟普通CSS继承的兜底逻辑一致。
4. H5开发中最容易踩的继承坑,以及标准解法
4.1 表单控件全套 font: inherit
H5页面里,表单控件是继承问题的高发区。input、button、select、textarea在UA样式表里都有自己的字体设置,导致你在body上传下去的font-family和font-size往往进不去。
标准做法是在全局reset阶段统一处理:
button, input, select, textarea { font: inherit; color: inherit; }这里的font: inherit是简写,会一口气把font-family、font-size、font-weight、font-style、line-height等都置为跟随父元素。实际项目中我还会把optgroup也带上,因为select里的选项组有时也会出现字体不统一。
这里补充一个H5特有的现象:iOS上input的font-size如果小于16px,聚焦时Safari会自动放大输入框里的文字到16px左右,防止用户误触缩放。很多文章把它归到继承问题,其实它跟继承无关,而是浏览器对可编辑元素的一种兜底处理。如果你遇到“输入框一开始字体是对的,聚焦瞬间变大”,优先检查是不是字号小于16px,别在继承里找半天。
4.2 a标签颜色:想继承先声明 color: inherit
a标签默认颜色是蓝色,而且带下划线,这些都是UA样式写的。父容器设置color传不到a上,哪怕你在body里写了color: #333,所有链接照样还是浏览器默认蓝。
如果产品要求链接颜色跟随正文,必须显式声明:
a { color: inherit; text-decoration: none; } a:hover { color: var(--link-hover, #1677ff); }这里要额外注意hover状态。当你给a写完color: inherit后,它跟正文颜色一致了,但hover反馈也消失了,因为a:hover其实也是一个显式规则;如果你不单独处理hover,用户根本看不出来链接可点击。我一般会把链接色提取成CSS变量,hover时换成另一个变量值,避免在多个页面重复写颜色。
4.3 font-size 的计算值继承,引发的 em/rem 混用问题
font-size是所有继承属性里最需要小心计算的一个。它继承的是计算后的px,而不是原来的百分比或em值,但子元素自己设置font-size时,如果是em,又会基于继承到的font-size重新计算,这就产生了“套娃放大”。
看这个嵌套:
<div style="font-size: 1.2em"> <div style="font-size: 1.2em"> <p style="font-size: 1.2em">三层嵌套后的字号</p> </div> </div>如果根默认16px,三层1.2em叠乘后大约是27.6px,而不是很多人直观以为的“跟前两层一样”。越往下套,字号越夸张,这就是em的继承放大效应。相比之下,rem始终相对于根元素的font-size,一层嵌套再深也不会产生叠乘。
我给的工程建议很简单:
- 全局文字基准用rem,好处是参考点唯一,不会有中间层的计算干扰。
- 组件内部与文字相关的局部间距、模块缩放,可以局部用em,比如按钮内边距随字号放大。
- 不要把em用在全局多层级嵌套的布局尺寸上。
还有一个常见误判:以为给body写了font-size: 62.5%,rem也会跟着缩放。其实rem永远认html根元素,body的字号变化只影响那些继承font-size的后代,跟rem没有任何关系。如果你发现某个用rem的元素没有随着body缩放,那不是bug,而是rem本身的设计如此。
4.4 列表样式继承和表格属性继承的边界
列表序号的继承也有迷惑性。list-style属性本身是继承的,在ul上设置list-style: none,绝大多数情况下li的序号会消失。但有些工程会在reset阶段给ul和ol各自设置list-style-type,或者直接把序号样式写在li上,这时候就会出现“父级明明写了none,子级还是有点”的情况。
稳妥的reset写法:
ul, ol, li { list-style: none; margin: 0; padding: 0; }后续某个地方需要有序号时,再单独给ul或ol写list-style-type。这么做比只写ul { list-style: none }更抗造,因为直接堵死了li层面的显式设置。
表格属性的继承边界也值得注意。border-collapse、border-spacing、caption-side这些是继承的,但border本身不继承。很多人以为在table上设置border后单元格自动有边框,实际不是,单元格边框要显式去画。如果想让表格整体采用折叠边框,只需要在table上写border-collapse: collapse,内部单元格默认会跟随这个应用模式,不必每个td都写一遍。
4.5 用 currentColor 把“继承来的颜色”用到边框、背景、伪元素上
继承下来的color除了直接影响文字颜色,还能通过currentColor这个关键字把颜色“借用”给其他属性。currentColor的意思是“当前元素color的计算值”,如果当前元素的color是通过继承得到的,那currentColor拿到的就是继承来的那个颜色。
于是可以做很多非常省事的联动:
.link-underline { color: inherit; /* 跟随正文颜色 */ border-bottom: 1px solid currentColor; }这样链接的下划线颜色会跟着文字颜色走,不用额外写border-color。类似的场景还有伪元素图标、加载动画、卡片的描边等。
再进阶一点,配合color-mix可以做“同色系半透明”背景,而且颜色源还是继承来的:
.button { color: var(--theme, #1677ff); border: 1px solid currentColor; background: color-mix(in srgb, currentColor 8%, white); }这套写法在H5现代浏览器里已经足够稳,做出来的按钮描边和文字天然同色,换主题色时不需要再单独改背景或边框,维护成本低很多。
5. 继承问题的调试与团队实践
5.1 DevTools 里的 Inherited from 到底怎么看
排查继承问题最快的工具还是Chrome DevTools。选中一个元素后,右侧Styles面板往下拉,通常会出现一个“Inherited from xxx”的折叠区块,里面列的就是从某个父级继承下来的声明,点击父级跳转可以直接看到来源。
如果你要查某个具体属性的最终值,切到Computed面板更直观。在筛选框输入属性名,比如font-size,面板会显示这个元素在该属性上的最终计算值,同时标注它的来源是“Inherited from div”还是某条显式规则。这里有一点要留意:只有当某个值确实是通过继承拿到的,面板才会标注来源;如果它被一条显式规则覆盖了,Computed里展示的是覆盖后的值,不会同时告诉你“本来还能继承一个别的值”。
我自己排查时的顺序很简单:
- 选中元素,打开Computed。
- 搜出现象对应的属性名。
- 看来源标注是Inherited from还是某条规则。
- 如果是继承值,点进父级,看父级这个属性有没有被更高覆盖。
- 查父级没有就直接找根元素和全局reset。
这套流程每次都能在几分钟内定位到问题源头。最忌讳的是在Styles面板里一层一层翻找“谁覆盖了谁”,那些看似冲突的规则很可能根本不是问题真正的起点。
5.2 常见继承问题速查表
| 现象 | 原因 | 解法 |
|---|---|---|
| 页面正文文字正常,按钮/输入框字体怪异 | 表单控件有自己的UA字体样式 | button, input, select, textarea { font: inherit; color: inherit; } |
| 给父容器设置color,a标签还是蓝色 | 浏览器默认给a设置了color | 显式写 a { color: inherit; } 并处理hover |
| 某段文字颜色被全局样式改成意外颜色 | 通配符等显式规则压过了继承值 | 检查* {}、reset、第三方全局样式 |
| 多层嵌套后文字越变越大 | em一次次基于继承字号叠乘 | 全局字号改用rem,局部间距才用em |
| 列表序号在父级改不掉 | 序号可能直接写在了li或UA样式里 | ul, ol, li 统一 reset list-style |
| iOS输入框聚焦时文字自动变大 | 输入框字号小于16px | 把input的font-size至少设为16px |
| 按钮边框/伪元素颜色跟文字不一致 | 没有使用currentColor | 用border: 1px solid currentColor 桥接 |
表格里第一行和第三行最容易同时出现。我之前接手过一个老项目,全局写着* { color: #666 },结果所有继承链在第一个层级就被掐断了,body上设置的任何主题色都进不了子孙,最后只能一层层找通配规则。所以排查继承问题的时候,别只看父子有没有冲突,先去全局“犯罪现场”里翻一遍通配符。
5.3 工程化建议:把继承变成可控的变量体系
继承不是用来“赌运气”的机制,它是可以被设计好的。我在项目里定的规矩是:文本基础样式只写在html和body上,表单和链接用inherit关键字显式收编,所有主题相关的颜色和尺寸统一走CSS变量。
先建一套基础变量:
:root { --c-text: #333; --c-theme: #1677ff; --fs-body: 14px; --lh-body: 1.6; } body { color: var(--c-text); font-size: var(--fs-body); line-height: var(--lh-body); } button, input, select, textarea { font: inherit; color: inherit; } a { color: var(--c-theme); }这套体系的好处是:新页面不用再重复声明body的字体和颜色,按钮和输入框自动跟随,链接也有一个独立的主题色入口。改主题时只需要动:root里的变量,不需要进到每个样式文件里改颜色值。
自定义属性天生具备继承能力,所以值得把“可能随上下文变化”的值都提升到继承链路上。例如某个卡片内部按钮要换主题色,就在卡片容器上覆盖变量,内部组件通过var()自动响应,不需要额外写权重更高的规则和选择器。这样既减少了死选择器数量的膨胀,也让继承从一个“容易踩的坑”变成了“可控的架构工具”。
我自己在项目里定了这么一条规矩:能用var()表达的就不要直接写死,能通过inherit显式声明就不要依赖默认值。写CSS不是写作文,少一点“魔法”,多一点显式意图,后面接手维护的人会轻松很多。这是我在无数次被继承问题坑过之后,最想强调的一点。