结论:CSS 选择器优先级是一个四元组 (行内样式, ID, class/属性/伪类, 标签/伪元素),比较时从左到右逐位比较,进位不适用;任何一位大就赢,与总“分数”无关,这就是 .box(0,0,1,0) 能压过 11 个 div(0,0,0,11) 的原因。
CSS 选择器优先级怎么计算?
结论:把选择器拆成四列计数,得到形如 (a,b,c,d) 的四元组,逐位比较即可。
四列的对应关系如下:
- a:行内
style="..." 属性,出现记 1,不出现记 0
- b:ID 选择器
#id 的数量
- c:class
.cls、属性 [type="text"]、伪类 :hover/:nth-child() 的数量
- d:标签
div、伪元素 ::before/::after 的数量
通配符 *、组合符 >、+、~、空格后代符,以及 :where(),全部计 0,对优先级没有任何贡献。
举例:#nav .item a:hover 拆出来是 1 个 ID、2 个 class/伪类(.item、:hover)、1 个标签(a),即 (0,1,2,1)。对比 .menu .item a.link,即 (0,0,3,1),第三位 1 大于 0,所以前者胜出,颜色按前者渲染。
为什么 11 个标签选择器赢不过 1 个 class?
结论:四元组逐位比较,高一位的 1 就足以压制低位的任意数量,不存在“十进一”。
很多人习惯把 ID 记作 100 分、class 记 10 分、标签记 1 分,这在选择器数量少于 10 时结论正确,一旦 tag 数达到 11 就翻车:
div div div div div div div div div div div { color: red; } /* (0,0,0,11) */
.box { color: blue; } /* (0,0,1,0) */
结果文字是蓝色。因为比较从 b 列开始,第三位 .box 有 1、div 链是 0,比较在第三位就已结束,第四位填多少都不影响。写样式时不要把“总分”当成排序依据。
行内样式和 !important 谁优先?
结论:!important 不属于优先级体系,它改变的是层叠(cascade)的权重来源,优先级只在同等重要性之间才比较。
完整的层叠排序(从高到低)是:用户代理样式表与作者样式表里的 !important 声明 → 行内 !important → 作者样式表普通声明 → 行内普通声明 → 用户代理默认样式。实际开发中记住两条就够:行内普通样式能压过所有普通选择器;带 !important 的普通选择器能压过行内普通样式。
注意两个 !important 之间仍然比优先级,谁的四元组大谁赢;所以到处撒 !important 只会把冲突推到更深的层级,最终无法解决。真正优先级冲突优先用“提高选择器精度”或“把样式放进更晚的 @layer”来解决。
:is()、:not()、:has()、:where() 的权重怎么算?
结论::is()、:not()、:has() 取括号内参数里优先级最高的那个作为自身权重;:where() 的权重恒为 (0,0,0,0)。
也就是说 :not(#main .item) 的权重等同于 #main .item,即 (0,1,1,0),而不是伪类的 (0,0,1,0)。:where(.a, #b) 无论写什么参数都计 0,非常适合写“可被业务样式覆盖的默认皮肤”。
::before、::after、::first-line 这类伪元素属于第四列 d,:hover、:focus、:nth-child(2n) 这类伪类属于第三列 c,别把冒号和双冒号混为一谈。
怎么快速验证优先级判断对不对?
结论:用浏览器开发者工具(DevTools)的 Styles 面板看被划掉的行,是最快的验证手段,不用靠猜。
步骤:F12 打开 DevTools → Elements 面板选中目标元素 → 右侧 Styles 标签,被划掉的声明就是被更高优先级或更靠后的同权重规则覆盖的;切到 Computed 标签可以看到最终生效值和来源文件行号。同时检查顺序规则:两个选择器四元组完全相同时,样式表里后出现的胜出,这就是为什么第三方 UI 库的样式经常需要放在业务样式之前引入。
日常维护建议:单个页面避免出现 3 层以上的后代选择器;ID 只用于锚点和 JS 钩子,不用于写样式;组件内统一用单层 class 加 :where() 做默认值,基本可以消灭 90% 的“样式不生效”。
本站的「前端开发」版块下设有专门的 CSS 分类,遇到优先级排查不了的情况可以直接在那里贴代码求助。发帖前记得先上传头像,未上传头像的账号无法发帖;发帖冷却为 30 秒,编辑好内容再提交,别被冷却卡住。
收束一句:优先级是四元组逐位比,不是分数累加;!important 与行内样式比的是层叠不是优先级;函数式伪类看参数,:where() 恒为零。