⭐ 推荐:社区规则条款 V1.0

CSS 选择器优先级计算规则与最佳实践

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客
发布于 2026-10-12 12:19 ·2 浏览 ·2 回复
内容摘要

讲解 CSS 选择器优先级按内联、ID、类/属性/伪类、元素/伪元素构成四元组逐位比较,组合符不计权重,并说明 :where、:is、:not、:has 的特殊规则、开发者工具验证方法,以及优先级与来源、@layer、书写顺序的层叠关系,建议通过扁平化权重和分层管理避免样式失控。

学完这篇你能得到一套可直接套用的 CSS 选择器权重计算法,以及让样式表不越写越乱的实操习惯。

第一步:记住四元组 (a, b, c, d)

CSS 优先级本质是一个四位计数,从左到右逐位比较,高位大的直接赢,不用看后面:

  • a —— 内联样式,写在标签 style="" 里的算 1
  • b —— ID 选择器,如 #header
  • c —— 类 .btn、属性 [type="text"]、伪类 :hover / :nth-child() / :focus-visible
  • d —— 元素 div、伪元素 ::before / ::after

看两个例子:

#nav .item a:hover { }   /* (0,1,2,1) */
ul li.active { }         /* (0,0,1,2) */

前者只有 b 位是 1,后者是 0,所以前者赢,后面几位不用比。

注意:*、组合符 > + ~、后代选择器的空格,全都不计入权重。写 div > p 和写 p 的权重是一样的 (0,0,0,1)。很多人靠堆层级来"提权",其实白写。

第二步:在浏览器里验证,别靠脑算

打开 Chrome / Edge 开发者工具(F12),切到 Elements → Styles 面板,鼠标悬停在任意一条规则的选择器上,会浮出 specificity: (0, 2, 1) 这样的提示。被划掉的属性就是被更高权重的规则打败的。

这是排查"样式为什么不生效"最快的路径:先看目标属性有没有被划掉,被划掉就往下找那条赢了的规则,直接对比权重。

第三步:处理三个特殊角色

:where()、:is()、:not()、:has() 的权重规则和普通伪类不同:

  • :where(...) 的权重恒为 0,括号里写多复杂都不算
  • :is(...) :not(...) :has(...) 取括号内参数里最高的那个
  • 伪元素 ::before 计入 d 位
:is(#a, .b) p { }   /* (0,1,0,1),取 #a 的 1 个 ID */
:where(#a, .b) p { } /* (0,0,0,1),归零 */

注意:内联 style="" 的 a 位只有 !important 能压过去,普通选择器权重堆到天上也没用。

第四步:分清「优先级」和「层叠顺序」

这是最容易混的一步。CSS 的层叠判定分三层,优先级只是其中一层:

  1. 来源与重要性:!important 用户样式 > !important 作者样式 > 作者样式 > 用户样式 > 浏览器默认样式
  2. 选择器权重:即上面的四元组
  3. 书写顺序:权重完全相同时,后写的赢

另外还有 @layer 层叠层,它插在"来源"和"权重"之间:

@layer base, components, utilities;

@layer components { .btn { color: red } }
.btn { color: blue }   /* 未分层的样式赢 */

注意:未分层(unlayered)的样式永远压过分层样式,哪怕层里的选择器是 #id。所以把第三方库、重置样式统统塞进 @layer base,你后面写的组件样式天然就能覆盖它们,不用再拼权重。层里的 !important 会反转层顺序,这个特性别乱用。

第五步:六条最佳实践

  1. 权重扁平化:整体控制在 (0,1,x) 以内,绝大多数样式用单个类就能选中
  2. 样式钩子不用 ID:ID 留给 JS 的 getElementById 和页面锚点,一旦用 ID 写样式,后面全得靠 !important 追
  3. !important 当补丁 = 技术债:能用层或命名空间解决的,绝不加 !important
  4. 覆盖第三方样式用 :where():写成 :where(.第三方类) .我的类,你的权重就固定在 (0,1,0)
  5. 分层管理:@layer reset, vendor, base, components, utilities; 一行声明顺序,后面顺序固定
  6. 命名规范化:BEM(.block__element--modifier)保证每个类都是同级权重,谁覆盖谁只取决于书写顺序,可预测

第六步:拿一段代码自测

a { color: black }                    /* (0,0,0,1) */
.nav a { color: red }                 /* (0,0,1,1) */
.nav .list a { color: blue }          /* (0,0,2,1) */
.nav .list a:hover { color: green }   /* (0,0,3,1) */

四个规则作用在同一条链接上时:不悬停是 blue,悬停是 green。如果想让 a 那条赢,只能靠 !important 或改成 :where() 结构——这就是为什么一开始就别让权重失控。

小结

  • 优先级是四元组 (a 内联, b ID, c 类/属性/伪类, d 元素/伪元素),从左往右比
  • *、组合符、空格组合器不计数;:where() 权重为 0,:is() / :not() 取最高
  • 优先级只是层叠三层里的中间一层,前面还有来源与重要性,后面还有书写顺序
  • 未分层样式压过分层样式,用 @layer 一键压制第三方库
  • 实践准则:权重扁平、不用 ID 做样式钩子、慎用 !important、命名规范
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-802.html
转载请注明出处,版权归原作者所有。

全部回复 2

陈先生
陈先生 正式会员正式会员认证极客认证极客 1楼 2026-10-12 12:22

四元组这套算法没问题,但有两个后面常踩的坑没摊开,补一下。一是 :nth-child(An+B of S) 里 of 后面的选择器是要算权重的,而且是和伪类本身的 (0,1,0) 叠加——:nth-child(2n of .item) 实际是 (0,2,0),不是大家以为的 (0,1,0),用它可以做"按条件提权"的细活。二是 :where() 归零这条有例外:把 !important 写进 @layer 之后,层顺序会反转,先声明的层反而压过后声明的层,也就是 @layer base 里的 !important 会盖住 @layer utilities 里的。所以在分层里随手写 !important,很容易做出一个谁都盖不住的东西,建议只在极少数兜底场景用。

另外补一个落地层面的提醒:@layer 是 Chrome 99 / Safari 15.4 / Firefox 97 才支持,老浏览器遇到不认识的 @layer 会整块丢弃,不是降级而是全丢。要兼容老设备的话,关键样式得留一份不分层的兜底,或者干脆只在新项目里上。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #698 2楼 2026-10-12 12:32
陈先生:四元组这套算法没问题,但有两个后面常踩的坑没摊开,补一下。一是 `:nth-child(An+B of S)` 里 `of` 后面的选择器是**要算权重**的,…

你补的这三条我全部认同,尤其 of S 那条确实是最容易算错的地方。

关于 :nth-child(An+B of S):权重是「伪类本身 (0,1,0) + 参数里最具体的那个选择器」,所以 :nth-child(2n of .item) 是 (0,2,0),:nth-last-child(2n of #list li) 则是 (0,1,1,1)。同样算法也适用于 :is() / :not() / :has(),关键细节是取参数中的最高值,不是累加——:is(.a, .b) 只有 (0,1,0),不会叠加成 (0,2,0)。这点和 of S 的叠加规则正好相反,容易记混。

关于 !important 在层里反转:这个坑比想象中隐蔽,再补一刀——反转后未分层(unlayered)的 !important 在作者样式里反而是最弱的一档,任何分层里的 !important 都能盖住它,只有在普通声明里它才反过来压过分层。所以当你发现一条带 !important 的样式莫名压不过别人,先查它是不是没分层。

关于兼容性:老浏览器把 @layer 当未知 at-rule,确实是连块带内容整块丢弃,不是降级。落地就是你说的两条路——关键兜底样式留在最外层不分层,或者构建期用 PostCSS 的 cascade-layers 把层拍平。顺带一提,@supports 在这里帮不上忙,它检测不了 at-rule 本身。

最后一个小延伸:`@layer a, b;