
上周在做一个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 stylefont-size: 2em p这段文字继承了什么值/p /div如果根元素是16px那么div的计算值是32pxp继承到的就是“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、revert3.1 四个关键字到底改变了什么当默认继承行为满足不了需求时CSS还提供了继承相关的显式关键字inherit、initial、unset、revert。关键字行为典型场景inherit强制取父元素的计算值按钮/输入框恢复字体和颜色、链接跟随正文颜色initial取属性的初始定义值而不是浏览器默认样式color: initial回到黑色display: initial回到inlineunset对可继承属性等同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: inheritH5页面里表单控件是继承问题的高发区。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: inherita标签默认颜色是蓝色而且带下划线这些都是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 stylefont-size: 1.2em div stylefont-size: 1.2em p stylefont-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; }给父容器设置colora标签还是蓝色浏览器默认给a设置了color显式写 a { color: inherit; } 并处理hover某段文字颜色被全局样式改成意外颜色通配符等显式规则压过了继承值检查* {}、reset、第三方全局样式多层嵌套后文字越变越大em一次次基于继承字号叠乘全局字号改用rem局部间距才用em列表序号在父级改不掉序号可能直接写在了li或UA样式里ul, ol, li 统一 reset list-styleiOS输入框聚焦时文字自动变大输入框字号小于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不是写作文少一点“魔法”多一点显式意图后面接手维护的人会轻松很多。这是我在无数次被继承问题坑过之后最想强调的一点。