CSS 写进样式表之后,并不会立刻变成页面上的颜色、尺寸和位置。浏览器要先找出所有可能影响某个元素的声明,再按固定顺序处理冲突,最后才能算出每个属性的实际值。
所以,当一条规则没有生效时,先别急着继续加选择器。我们可以顺着浏览器的判断过程问四个问题:

你可以把“层叠”理解成完整的裁判流程,“优先级”只是流程后段的一项比较指标。“继承”走的是另一条取值通道:元素没有直接胜出的声明时,某些属性可以从祖先那里取得值。三者会一起影响结果,但不是同一件事。
先看一个最简单的例子。下面两条声明写在同一个规则块里。在其他条件完全相同时,同一属性后写的值会获胜,所以标题显示为蓝色。如果标题里还有 span,其中的文字通常也会变蓝,因为 color 可以从父元素向下继承。
h1 {
color: red;
color: blue;
}这里有个容易被忽略的细节:层叠比较的最小单位是声明,不是整个规则块。假如一个规则同时设置了 color 和 background,另一个规则只在 color 上与它冲突,浏览器会分别处理这两个属性。前一个规则的颜色落败,不会连带让背景失效。
.notice {
color: #334155;
background: #fef3c7;
}
main .notice {
color: #7c3aed;
}结果是:颜色来自 main .notice,背景仍然来自 .notice。我们后面判断冲突时,也要始终盯住“同一个元素、同一个属性”,不要把整个规则块捆在一起看。
选择器优先级通常写成 (a, b, c)。三列分别记录:
a:ID 选择器的数量,例如 #dialog。b:类选择器、属性选择器和伪类的数量,例如 .card、[open]、:hover。c:类型选择器和伪元素的数量,例如 article、::before。比较时要从左向右逐列看,先出现差异的那一列直接决定结果。不要把三列当成十进制数字,也没有“十个类进位成一个 ID”这种规则。哪怕后两列再大,(0,99,99) 仍然低于 (1,0,0)。

h1 /* (0,0,1) */
body h1 /* (0,0,2) */
h2.topic /* (0,1,1) */
#answer /* (1,0,0) */
div#sidebar a[href]::after /* (1,1,3) */空格、>、+、~ 这些组合符只负责描述结构关系,不会增加权重。通配选择器 * 和 :where(...) 的贡献都是 (0,0,0)。不过,权重为零不等于没有匹配元素,等讲到继承时你会看到这个区别为什么有用。
几个函数伪类的算法稍有不同,可以单独记下来:
:where(...) 的整体权重永远是 (0,0,0)。:is(...)、:not(...)、:has(...) 取参数列表中最高的那一个优先级,而不是把所有参数相加。:where(main, #app) article /* (0,0,1) */
:is(aside#warn, code) /* (1,0,1) */
.card:not(#featured, .old) /* (1,1,0) */#panel 和 [id="panel"] 可能匹配同一个元素,权重却不一样。前者计入 ID 列,结果是 (1,0,0);后者仍是属性选择器,只计入中间一列,结果是 (0,1,0)。
算优先级之前,先确认前面的层叠条件已经打平。高优先层里的低权重选择器,可以压过低优先层里堆了许多 ID 的选择器。只看选择器分数,很容易判断错。
!important一个选择器算出的优先级,会落到规则里的每条声明上。遇到分组选择器时,可以先把它想成几条独立的规则:
h1,
h2.section {
color: silver;
background: black;
}匹配 h1 时,color 和 background 两条声明都带有 (0,0,1);匹配 h2.section 时,它们带有 (0,1,1)。如果同一个元素同时匹配了分组中的多个选择器,这次匹配会使用其中最高的优先级。
重复类、属性甚至 ID,确实能人为抬高权重,例如 .button.button.button。语法没有问题,但下一次想覆盖它时往往还得继续加码,样式就会越来越难改。遇到这种情况,先看看能不能用层叠层划分范围,或者给组件和状态设置更明确的边界。
再来看 !important。它标记的是单条声明:
.dialog {
color: #111827 !important;
background: white;
}这里只有 color 属于重要声明,background 仍是普通声明。标记要写在值的末尾、分号之前。浏览器会先把重要声明和普通声明分开比较;在相同来源和上下文中,普通声明的选择器分数再高,也越不过重要声明。只有双方都带 !important,才会继续比较来源、层、优先级和顺序。
!important 会直接改变声明所在的层叠赛道。随手添加之后,组件覆盖和主题样式通常更难协调,还可能妨碍用户的无障碍样式。使用它之前,先检查层叠层、选择器边界和源码顺序。
继承说的是某些属性可以从父元素传给后代。文本相关属性通常会继承,例如 color、font-family、line-height;margin、padding、border、background 这类盒模型属性通常不会继承。想一想也很自然:如果容器的边框和外边距会自动传给每一层后代,嵌套几层之后,页面就会出现一圈又一圈重复的边框和空隙。

ul {
color: #475569;
border: 2px solid #94a3b8;
}在这个例子里,li、列表标记和更深层的文本会继承颜色,却不会自动得到 ul 的边框。值沿着 DOM 树向下传,不会从子元素反向传给父元素。
页面画布有少量特殊的传播规则。比如根元素没有背景时,浏览器可能把 body 的背景用于根画布。这属于画布背景的特殊处理,不要把它理解成普通的“子元素向父元素继承”。
理解继承时,有个区别一定要分清:继承值没有选择器优先级,连 (0,0,0) 都没有。 所以,一条直接匹配子元素、权重为零的规则,也能覆盖继承来的值:
h1#title { color: black; }
* { color: gray; }h1 里的 em 原本可以继承黑色,但 * 会直接匹配 em。于是,零权重的灰色声明仍然胜过“没有权重”的继承黑色。全局写 * { color: ... } 时要格外小心,它很容易在无意中截断继承链。
链接经常会暴露这个问题。即使工具栏已经设成白色,里面的链接仍可能被浏览器的 :any-link 规则直接着色。解决办法是直接声明链接的颜色,或者明确让它取父元素的值:
#toolbar {
color: white;
background: #111827;
}
#toolbar a:any-link {
color: inherit;
}这里的 inherit 来自一条直接匹配链接的规则,所以这条声明本身带有选择器优先级。等它在层叠中胜出后,浏览器再从父元素取得颜色。
现在把前面的概念串起来。遇到冲突时,可以顺着下面的顺序排查:
CSS 的来源分为用户代理、用户和作者。普通声明通常按作者、用户、用户代理的顺序排列;到了重要声明,这个来源顺序会反转,用来保护用户自己的设置。再把过渡和动画放进来,我们可以得到一份由强到弱的实用列表:
按照这个顺序,两个常见现象就好理解了。普通内联样式可以压过样式表中的 ID 选择器;样式表里的作者重要声明,又可以压过普通内联样式。如果内联声明本身也带有 !important,进入同来源、同层的比较后,直接附着在元素上的声明会占优。
Shadow DOM 这类封装上下文也会参与排序。组件内部声明可以控制内部元素,外部的继承值仍能进入组件。调试 Web Components 时,选择器分数只是其中一环,还要看声明位于封装边界的哪一侧。
HTML 本身还可能提供非 CSS 的呈现提示,例如 width、height、hidden,以及早已不推荐使用的 font 元素。现代浏览器通常把这类提示放在接近用户代理样式的位置,因此作者或用户 CSS 往往能够覆盖它们。不过,语义属性仍然要按语义使用;能被 CSS 覆盖,不代表我们应该重新依赖过时的呈现标记。
@layer 让我们先安排好不同样式区域的覆盖关系。项目变大之后,这比不断增加选择器权重或补 !important 更容易维护。
@layer reset, components, page;
@layer reset {
button { font: inherit; }
}
@layer components {
.button { color: navy; }
}
@layer page {
.button { color: rebeccapurple; }
}对于普通声明,后声明的层更强:page 高于 components,components 又高于 reset。没有放进任何命名层的普通声明位于隐式最终层,比所有命名层中的普通声明都强。这样一来,低优先层中的选择器即使分数很高,也没有机会和高优先层中的选择器比较权重。
重要声明的层顺序恰好反过来:越早声明的命名层越强,而且命名层中的重要声明高于未分层的重要声明。这样安排,是为了让基础层中的保护性约束不容易被后加载的代码改写。

可以在 @import 时直接分层:
@layer site, components, page;
@import url("/css/library.css") layer(components);同名层即使分成多个块书写,最后也会合并到同一层。匿名层同样参与排序,但后面无法通过名称继续追加。实际项目里,如果后续还要扩展,命名层通常更方便。
如果层、来源、重要性、附着方式和优先级都相同,就轮到源码顺序决定结果,后出现的声明获胜。可以把 @import 导入的内容看成插在导入语句所在的位置,因此当前样式表中后写的同权重规则会覆盖导入文件中的规则。
链接可能同时处于多个状态,所以这些规则的书写顺序需要和预期效果一致:
a:link { color: blue; }
a:visited { color: purple; }
a:focus { color: green; }
a:hover { color: red; }
a:active { color: orange; }这些选择器的优先级都是 (0,1,1)。一个正在被点击、尚未访问的链接,可能同时满足 :link、:focus、:hover 和 :active。前面的条件打平后,后写的 :active 就会获胜。如果各个状态修改的是不同属性,它们之间没有同属性冲突,书写顺序自然也不会造成覆盖。
如果想把意图写得更明确,也可以组合互斥的状态:
a:link:hover { color: red; }
a:visited:hover { color: gray; }这两个选择器权重相同,但一个链接不可能既已访问又未访问,所以它们不会互相竞争。如果再加入 :active,同一个元素可能同时匹配 hover 和 active,此时仍要通过书写顺序或更明确的状态组合来处理冲突。
当 DevTools 里的一条声明被划掉时,可以按下面的顺序检查。大多数时候,问题都能在继续增加选择器之前找到:
!important 与来源,再看内联附着、封装上下文和 @layer。CSS 很难做到永远没有冲突,我们真正需要的是让结果可以预测。大范围的覆盖关系交给层叠层,组件使用低而稳定的选择器,排版和主题尽量通过继承传递,局部变化则交给明确的状态类。只有确实需要切换重要性赛道时,再使用 !important。