
这个问题看似基础但我在面试和带新人时发现能把CSS优先级完整说清楚的人其实不多。很多人脱口而出id class 标签或者背出内联1000、id100、类10、标签1这样的口诀但真扔给他一段嵌套复杂的选择器让他算或者让他解释为什么一个看起来权重很高的选择器却被另一个选择器覆盖时就支支吾吾了。更麻烦的是网上大量流传的十位数、百位数相加的计算方式在现代浏览器里其实已经不准确了。Chrome 88之后浏览器内部已经迁移到更接近标准描述的选择器特殊性比较机制。这意味着如果你还抱着老一套权重数值相加的思路去排查样式问题会在某些边界case上栽跟头。这篇内容没有任何铺垫直接把你需要知道的东西一次说透层叠规则的本质、特殊性计算的正确方式、那些容易被忽略但真实影响优先级判断的变量以及我在真实项目里踩过的坑和排查方法。不管是准备面试还是日常写样式时需要跟样式覆盖问题搏斗都可以把这份东西当工具文收藏。1. 从为什么会有优先级说起层叠规则的本质在动手计算权重之前得先搞明白一件事CSS这门语言是怎么决定一个元素最终长什么样的。只有理解了这套底层逻辑你才能真正明白权重计算为什么是这样设计的而不是靠死记硬背。1.1 浏览器如何决定一个元素的最终样式CSS的全称是Cascading Style Sheets中文翻译是层叠样式表。关键词在Cascading也就是层叠。层叠的意思不是说多个样式简单地叠加在一起而是当多个规则同时命中同一个元素时浏览器需要一套裁决机制来决定到底哪个声明生效。这套裁决机制在CSS规范里称为层叠过程Cascade Process。它遵循一批优先级条件先比较来源和重要性再比较选择器特殊性最后比较出现顺序。大多数时候我们说的权重只是其中一环——选择器特殊性Specificity但在实际表现中来源、!important、出现顺序这些因素都会干扰最终结果。用生活化一点的类比假如你是一个项目的最终决策人手下有多个顾问给你提建议。谁的职位高谁的方案就容易被采纳来源和重要性职位一样高的时候谁的方案更有针对性、写得更具体特殊性谁说了算。如果这些都一样那就看谁最后一个提交方案出现顺序后提交的覆盖先提交的。1.2 样式来源不同优先级也不同很多人忽略了这一点同样一条规则来自不同发布渠道优先级是完全不同的。CSS样式的来源大致分三类作者样式也就是我们自己写在HTML文件里、外部CSS文件里、或者通过style标签定义的样式这是日常开发中打交道最多的类型。用户样式用户浏览器自带的样式设置包括阅读器样式、某些浏览器插件注入的样式。普通用户很少主动设置但在无障碍场景中比较常见。用户代理样式UA样式浏览器默认样式。比如你在HTML里写一个h1不写任何CSS它看起来也比正文大、加粗这就是浏览器默认样式表在起作用。三者的优先级顺序是作者样式 用户样式 UA样式。但一旦出现了!important顺序会反转作者!important 用户!important UA!important。我在实际工作中见过一个很经典的坑有些开发者为了让某个样式必生效喜欢到处加!important结果遇到用户通过浏览器插件或浏览器自带的强制深色模式调整样式时自己页面上的样式反而被覆盖了。这不是CSS的bug而是层叠规则本来就规定用户对可访问性的需求优先级高于开发者。碰到这种情况别硬对抗正确做法是尽量避免滥用!important让自然优先级完成工作。2. 权重三段式id、类、标签的计数法前面说的是宏观的层叠规则。接下来进入核心话题——选择器特殊性Specificity也就是我们常说的权重。这是判断同一来源、非!important声明谁能生效的关键。2.1 特殊性三元组每一位是怎么数的根据CSS标准每个选择器都有一个特殊性值可以用三位数来表示不同维度上的计数通常写成(a, b, c)的形式aID选择器的数量。例如#header、#sidebar .nav里的#header和#sidebar都算。b类选择器、属性选择器、伪类的数量。例如.active、[typetext]、:hover、:focus都归这一类。c类型标签选择器和伪元素的数量。例如div、span、p以及::before、::after。比较规则是从高位到低位逐个比较。先比aa大的胜出a相同则比bb也相同则比c三个值都完全相同则后出现的那条规则胜出。例如/* 特殊性 (1, 0, 0) */ #content p { color: red; } /* 特殊性 (0, 1, 0) */ .container p { color: blue; }这两个规则都命中某个段落文本时#content p因为a位是1直接胜出段落是红色。不管.container p里包含多少个类选择器只要没有id选择器它在a位就已经输了。网上流传很广的id100class10标签1相加比较大小的说法本质上是把三元组强行转换成一个百位数。这个简化模型在大多数简单场景下够用但它在进位问题上是有缺陷的。比如十个类选择器相加得到100按这个简化模型就和id选择器平起平坐了但实际上十个.xxx类选择器的特殊性是(0, 10, 0)跟(1, 0, 0)比较时a位直接输掉类再多也没用。现代浏览器中特殊性是逐位比较的根本不存在进位。2.2 伪类、属性选择器、通配符、兄弟选择器都怎么归类我整理了不同选择器的归类明细方便对照选择器类型举例计入维度ID选择器#app、#main-titlea类选择器.error、.highlightedb属性选择器[typeradio]、[disabled]b伪类:hover、:focus、:nth-child(2)b标签选择器div、a、lic伪元素::before、::placeholderc通配选择器*不计入任何维度组合器后代、、、~不计入任何维度:not()、:is()、:has()内部参数参与计算以参数中最高的特殊性计入这里有两个特别需要记住的点。第一:not()里的选择器是参与计算的。一条规则的特殊性等于它内部参数的特殊性。例如/* 特殊性 (0, 1, 1)不是(0, 0, 1) */ p:not(.intro) { color: green; }:not()本身不贡献任何特殊性但括号里有个.intro所以它贡献了一个b位的计数。网上有一些老文章说:not()不参与权重计算这是不对的。至少从CSS选择器Level 3开始:not()的参数就一直参与计算。第二:is()和:where()是两个特殊的存在。:where()的特殊性永远为0不管括号里写了什么:is()的特殊性取括号里所有参数中最大的那个。举个例子/* 特殊性 (0, 1, 0) */ :is(.active, #main) { color: blue; } /* 特殊性 (0, 0, 0) */ :where(.active, #main) { color: red; }第一行里:is()括号里有一个id选择器#main所以:is()这一整块的特殊性按(1, 0, 0)计算等价于id选择器加一个类选择器的组合。第二行的:where()则是完全归零它存在的意义就是把你的选择器嵌套起来写但绝不影响权重方便做底层样式覆盖。2.3 进位陷阱一个流传已久的错误认知我必须单独用一小节来纠正a100、b10、c1相加比较这个说法因为它在很多中文教程甚至一些培训课程里都被奉为圭臬。网上流传的表格经常是选择器权重值内联样式1000ID选择器100类、属性、伪类10元素、伪元素1这个模型的错误在于它暗示了150大于100之类在极端情况下可能出现。比如说(0, 15, 0)这个组合按相加能凑出150跟(1, 0, 0)也就是id选择器的100相比好像更大但标准规定的比较方式是字典序逐位比较先看a位a位为0永远小于a位为1根本轮不到后面的位次参与比较。所以15个类选择器加在一起也赢不了一个id选择器。这个观念上的差异平时不会暴露因为没人会闲到写15个类名叠一起。但理解正确的比较规则能让你在面对复杂选择器时不靠估算数字更准确地判断胜负。正确的做法是把复杂选择器拆解成三元组逐位比较。比如ul#nav .item:hover a::before这条选择器包含1个id#nav2个类.item和:hover2个标签ul和a1个伪元素::before。特殊性是(1, 2, 2)。另一条.main .content #highlight a.active包含1个id#highlight3个类.main、.content、.active1个标签a。特殊性是(1, 3, 1)。两条规则相比a位都是1接着比b位3大于2所以第二条胜出。这种拆解方法比背数值相加靠谱得多。3. 那些改变优先级走向的高级变量!important、内联样式与继承选择器特殊性公式解决了同一来源、没有!important标记的情况。但在真实开发中内联样式和!important经常横插一杠子让整个比较过程复杂化。不了解它们的位置你会经常遇到明明我权重高为什么样式不生效的疑惑。3.1 !important 为什么会破坏权力平衡!important是CSS提供的终极优先级炸弹。它的实际效果是将声明提升到一个名为重要声明的独立分组中这个分组的优先级高于所有普通声明。在作者样式中一个!important声明能压过任何非!important的普通声明包括内联样式。所以严格来说比较优先级时要分两层看先看是不是重要声明带!important重要声明整体排在没有!important的普通声明前面。在同一组内比如都是普通声明或都是重要声明再按特殊性三元组和出现顺序比较。举个例子div idbox stylecolor: red;文本/div#box { color: blue !important; }这个文本最终是蓝色。因为作者样式里的!important优先级高于内联样式中的普通声明。这一点颠覆了很多人的直觉——他们以为内联样式永远最高但!important硬生生把作者样式优先级抬上去了。但我不建议你把!important当常规武器使用。它是层层覆盖问题下的兜底方案但一旦大面积使用你的样式表就失去可预测性了。比如你在一个公共组件库里给某个类加了!important使用方想通过自己的样式表微调组件外观会发现自己怎么调都调不动最后只能上!important对轰造成全球变暖式的权重军备竞赛。这轮军备竞赛的赢家只有一个就是不断膨胀的特殊性数字。3.2 内联样式与CSS选择器之间的对抗很多人在网上看到内联样式权重是1000的说法原因是把内联样式当作一个特殊的唯一选择器它专门有一个位次高于所有选择器。实际上标准里明确说内联样式在比较时不参与特殊性三元组而是独立于选择器特殊性之外优先级天然高于普通作者样式声明。理解这个区别非常重要不是内联样式等于1000而是内联样式根本不属于选择器特殊性体系它自成一级。既然自成一级任何选择器特殊性都无法超过它除非遇到!important。所以在实践层面给元素写内联样式前要想清楚这个样式要不要允许外部覆盖如果需要被主题定制或用户偏好覆盖就别写内联样式老老实实丢到类里去。我记得有一次给一个第三方组件库的内置弹窗调样式组件在JS里硬编码了一个top值到元素上我的CSS写了top: 100px !important才勉强覆盖后来发现组件版本升级后!important也盖不住了只能去翻组件源码改props。这种痛苦经历过一次你就会对内联样式产生敬畏。3.3 继承样式在优先级体系里的特殊地位继承样式是很多人搞不懂的一个点。它不是通过选择器命中的而是浏览器根据CSS的可继承属性规则从父元素传导下来的。例如color、font-size、line-height这类属性天然继承而margin、padding、border这类不继承。关键规则是任何显式匹配到元素的选择器声明优先级高于继承值。哪怕直系父级写上#parent .child .grandson这种权重极高的选择器继承来的颜色也打不过最简单的p标签选择器直接设置的color。div idparent stylecolor: red; p我是什么颜色/p /divp { color: blue; }上面的p标签最终显示蓝色。父级#parent的color: red即便来自id选择器到了子元素这里只是继承值优先级等于没有。而p { color: blue }是直接命中哪怕它的特殊性只有(0, 0, 1)也比继承值高。还有一个和继承相关的关键字需要区分inherit和unset。inherit显式要求继承父级计算值它会覆盖该属性本来已有的默认值unset则对该属性的继承性做重置——可继承属性表现同inherit不可继承属性表现同initial。了解这两个关键字能帮你写出更明确的样式意图尤其是在处理表单控件和HTML默认样式覆盖时。4. 真实项目中踩过的优先级坑常见误判与调试方法理论知识说完了下面分享几个我在项目里真实遇见的优先级翻车现场。这些case都不算高深但都很典型理解了它们能帮你少走弯路。4.1 踩坑案例一列表元素里永远不生效的hover我接手过一个后台管理项目侧边栏菜单的hover高亮样式怎么调都不生效。看代码是这样.menu-list li a:hover { color: #fff; background: #1890ff; }看起来没问题但它上面还有一条.sidebar .menu-list .item a { background: transparent; }十进制权重分别是多少第一条是(0, 3, 1).menu-list、li、a、:hover其中类包括.menu-list和:hover共2个类标签是li和a共2个标签等下我重新数一下。慢着ul.menu-list li a:hover其实应该是ul.menu-list li a:hover在实际项目里选择器大概率是.menu-list li a:hover。让我调整一下案例让它更严谨/* 规则A */ .menu-list li a:hover { background: #1890ff; } /* 特殊性 (0, 2, 2).menu-list、:hover 是两个类li、a 是两个标签 */ /* 规则B */ .sidebar .menu-list .item a { background: transparent; } /* 特殊性 (0, 3, 1).sidebar、.menu-list、.item 是三个类a 是一个标签 */规则B的特殊性是(0, 3, 1)规则A是(0, 2, 2)。比较b位3大于2所以在hover的时候规则B依然压着规则A背景色不会变。我当时第一反应是是不是hover写错了后来一查果然是特殊性被另一条规则稳稳压住。解决方法不是给hover拼命加类名而是降低规则B的权重或者给规则A再加上一个.item类名参与计算让它的b位至少达到3。最终我们选择给hover规则增加一点针对性.sidebar .menu-list .item a:hover { background: #1890ff; }这个选择器特殊性变成(0, 4, 1)稳稳压过规则B的(0, 3, 1)问题解决。这个案例的教训是碰到hover样式不生效先别急着怀疑是JS事件问题打开开发者工具看看到底是哪条规则赢了、为什么赢往往一分钟就能定位。4.2 踩坑案例二:not()在旧教材里留下的错误印象前面提过:not()参数参与特殊性计算但很多人被旧课程误导以为:not()完全不计入。有一次一个同事反馈一个复选框的自定义样式失效代码大致长这样.input-wrap input:not(:disabled) .custom-checkbox { border-color: #ccc; } input:disabled .custom-checkbox { border-color: #eee; cursor: not-allowed; }他说:not(:disabled)应该是没权重的为什么:disabled那条能把前面的覆盖掉实际上:not(:disabled)这个选择器中input贡献c位1:not()内部是:disabled:disabled是一个伪类属于b位所以:not(:disabled)整体特殊性是(0, 1, 1)。从结果来看前面的规则反而比后面的(0, 1, 1)相同但因为前面的规则在后一条之前所以正常情况应该是后一条赢符合预期这一点没毛病。但如果同事希望没禁用状态下的样式优先于禁用状态正确的做法不是拆::not()而是把:not()选择器内部的伪类换成特殊性更高或者改变选择器结构。这个case真正想说明的是不要凭直觉判断:not()的作用它在很大程度上就是把括号里的内容加入计算。括号里写一个类你就多一个类写一个id你就多一个id。4.3 用浏览器开发者工具确认谁赢了遇到优先级冲突时最快的方式不是看代码猜权重而是直接看浏览器怎么裁决。Chrome DevTools的Elements面板右侧选中目标元素Styles标签页会列出所有匹配的规则并且按优先级从高到低排序。被划掉的样式表示在当前层叠下未能生效鼠标悬停上去会显示是哪条规则覆盖了它。我排查优先级问题时固定会做的三件事在Styles面板里找到被划掉的属性点旁边的小箭头DevTools会直接显示被xx规则标记为优先一类信息直接从根源确认胜负关系。看规则列表顶部的element.style确认是不是有内联样式在起干扰作用。用Computed面板核对最终计算值避免被渲染层的其他因素干扰比如CSS变量、动画、过渡。这套流程用熟了你会发现大多数优先级问题不是数学题而是我根本没注意到还有一条规则也匹配了这个元素这种粗心题。5. 靠优先级打架不如靠规范写样式时的实战建议优先级规则的边界情况在这里基本讲完了。但你知道规则是一回事能不能写出自洽的CSS系统又是另一回事。以我的经验一个团队或者一个项目里的CSS如果总是在互相覆盖那大概率不是优先级没学懂而是代码组织出了问题。5.1 避免深度嵌套让选择器保持低权重从优先级的角度看最理想的状态是所有样式规则的特殊性都低而接近这样后续覆盖时的规则才可控。CSS方法论比如BEM之所以流行核心原因之一就是它能稳定地生产低特殊性选择器。BEM块、元素、修饰符的命名方式让每个类名都是单独的一个类没有标签、没有嵌套天然把样式特异性压到极低。举个例子div classcard card--highlighted h2 classcard__title标题/h2 /div.card__title { font-size: 18px; } .card--highlighted .card__title { color: #f00; }第一行特殊性(0, 1, 0)第二行因为多了一个类特殊性(0, 2, 0)覆盖关系清晰干净。如果哪天需要做主题定制第三方只要写一个同名的类或者更靠后的规则就能覆盖不会引发id大战。有些团队喜欢写.sidebar .menu .item a这种链式选择器看起来定位精准但特殊性随层级一路飙升后续想覆盖就得写更长、更深的链。长期下来项目里到处是几十个字符长、特殊性奇高的选择器谁也改不动。我的建议是能用单一类解决的问题绝不用后代选择器。5.2 命名规范与设计系统里的优先级管理命名规范不仅影响可读性直接影响优先级可控性。在组件化开发中如果组件根节点上都加了一个唯一的类比如.x-btn组件内部的任何元素需要个性化调整时你只需要在组件上再加一个状态类或者容器类就足以形成自然的层级关系完全不需要考虑层级嵌套。更有意思的一点是如果你的组件库支持主题定制那么你不仅要考虑组件本身的选择器设计还要预留一个可覆盖的入口。比如允许用户通过配置前缀、覆盖CSS变量等方式调整样式而不是让用户去和你的优先级博弈。这个思路特别重要因为它跳出了怎么提高我的权重这个局部问题转而思考怎么让整个系统的层叠更可预期。5.3 我自己的CSS组织习惯最后分享几个我在实际编码中的习惯不算什么高深理论但确实帮我省去了大量和优先级纠缠的时间习惯一日常开发中不多层级嵌套。无论是用预处理器SCSS/Less还是纯CSS限制嵌套深度在3层以内。SCSS里嵌套超过3层不仅编译产物里的选择器链长而且特殊性层层叠加给后续覆盖添堵。习惯二明确全局工具类的作用域。像.hidden、.text-center、.mt-8这种工具类我倾向在项目中单列一个文件集中管理避免散落在组件里。工具类的特殊性通常是(0, 1, 0)所有组件样式跟它竞争时胜负一目了然凡是需要覆盖工具类的场景我会在组件类里追加一个修饰类而不是去写!important。习惯三给关键交互状态预留正确的特殊性出口。比如按钮组件的:hover、:focus、:disabled状态在设计选择器时就确认好它们的原始特殊性。组件内部状态之间的优先顺序严格按默认态 交互态 禁用态来安排靠出现顺序辅助而不是靠堆权重。习惯四CSS变量的善用。颜色、间距、字体大小这些基础token优先用CSS变量定义。CSS变量本身不参与选择器特殊性计算但它能让你在不改变选择器结构的情况下通过覆盖变量值实现样式切换。这比增加选择器特殊性更优雅也更符合设计系统理念。6. 写在最后的排查心法调试优先级冲突不只是数学题更多时候是判断题。我在排查这类问题时的顺序是先看是不是来源顺序问题再看是不是内联样式或!important在做怪最后才是拿特殊性公式拆解选择器。这个顺序能让你在最常见的原因上快速收敛而不是每次都从最复杂的可能性开始排查。另一个值得养成的习惯是所有针对第三方库、组件库的样式覆盖都集中写在同一个覆盖文件里并清楚的注释为什么这个样式需要覆盖、用了怎样的特殊性策略。我自己就吃过注释不清的亏——三个月后回头看自己的代码看到一行莫名其妙的!important翻文档才回忆起是当年某个组件在特定版本下的bug那个版本早就升级修掉了。保留注释能帮你和你的队友省下大量考古时间。优先级计算本身不难难的是在真实项目中建立稳定、可预期的层叠体系。记住一个原则你在写CSS时选择的每一个选择器都在为这个体系投票。多给低特殊性方案投票少用高特殊性权术页面的样式才会长久可控。