写样式表时经常会遇到这样的需求:只有当元素同时具备某个class、处于某种状态、并且位于特定位置时,才应用某条样式。比如导航栏中"当前页"的链接是红色,但悬停时又变成深红,而如果这个链接同时被禁用了,颜色规则又要另算。单靠一个简单的类选择器显然做不到这么精细的控制,这时候就需要把多个选择器组合起来使用。CSS提供了多种组合方式,各自语义不同,用错了不但样式不生效,还可能影响整个页面的渲染。

交集选择器:多个条件同时满足才匹配
交集选择器是组合中最直接的一种,写法上就是把多个选择器紧挨着写在一起,中间不加任何空格或其他符号。例如p.active表示只匹配那些class中包含active的p元素,普通的p元素或者带active的div都不会被选中。如果再加上伪类,比如a.nav-link:hover,就表示必须是拥有nav-link这个class的a元素,并且鼠标正悬停在它上面,三个条件缺一不可。
这种写法非常实用,尤其适合在不能修改HTML结构、只能通过CSS区分元素的场景。举例来说,页面上有大量class为item的元素,但你只想给列表中最后一个item加特殊样式,就可以写成.item:last-child。再看一个属性选择器叠加的例子,input[type="text"].required会选中type属性为text且带有required类的输入框,可以用来给必填项加上特殊的边框颜色。
/* 必须同时是p元素、带active类、并且被悬停 */
p.active:hover {
color: #c0392b;
}
/* 列表项是最后一个子元素且带有特殊标记类 */
li.item:last-child {
border-bottom: none;
}
/* 文本输入框且必填 */
input[type="text"].required {
border-left: 3px solid #e74c3c;
}需要注意的是,交集选择器中如果包含标签选择器和类选择器,标签要写在前面,比如div.box而不是box.div。另外交集的组合数量没有上限,但每增加一个条件,选择器的特异性就会提高,这在后期覆盖样式时可能带来麻烦,所以条件够用就好,不要盲目堆叠。
并集选择器:用逗号让多条规则共用一份样式
与交集相反,并集选择器表达的是"或"的关系,多个选择器之间用逗号分隔,只要元素满足其中任意一个条件就会被匹配。最典型的用法是给多个元素设置相同的初始化样式,例如把h1到h6的margin统一清零,或者让多个不同结构的按钮共享一套外观。
/* 多个标题共用一份清除样式的规则 */
h1, h2, h3, h4, h5, h6 {
margin: 0;
font-weight: 600;
}
/* 不同状态的按钮共享颜色 */
.btn-primary,
.btn-primary:hover,
.btn-primary:focus {
background-color: #2980b9;
}并集选择器的每一部分都是独立计算的,逗号前后的选择器互不影响。这里有一个新手容易犯的错误:想在多个容器下选中同类子元素时,写成.sidebar .title, .footer .title是正确的,但如果图省事写成.sidebar, .footer .title,那么第一个选择器会匹配sidebar本身这个容器,而不是它内部任何东西,样式应用的范围就完全跑偏了。所以在使用逗号时,每个组合条件都要完整地写一遍,不能省略公共前缀。
CSS较新的规范还引入了:is()伪类,可以让并集写法更简洁。比如:is(h1, h2, h3) .content等价于三条规则并列,而且:is()内部的特异性的计算规则是取参数中特异性最高的一项,这一点与传统逗号写法略有差异,迁移老代码时需要留意。主流浏览器对:is()的支持已经相当完善,新项目中可以放心使用。
层级组合:后代、子代与相邻关系的精确控制
除了同一元素上的条件叠加,更多时候我们需要描述元素之间的关系。后代选择器用空格连接,如.card p表示card内部所有层级的p元素;子代选择器用大于号连接,如.card > p只匹配card的直接子元素p。两者的区别在于匹配深度,如果card里面又嵌套了一层section,section里的p用后代选择器能选中,用子代选择器就选不中。在结构复杂的组件中,优先使用子代选择器可以避免样式"泄漏"到深层嵌套的元素上。
相邻关系选择器则进一步扩展了控制能力。h2 + p匹配紧跟在h2后面的第一个p元素,常用于给标题下方第一段文字做特殊排版;h2 ~ p匹配h2后面所有同层级的p元素,范围更宽。这类选择器在无法添加额外class的场景下特别有价值,比如渲染富文本内容时,内容结构不可控,只能靠元素间的位置关系来定制样式。
/* 只影响卡片的直接子段落,不影响嵌套内容 */
.card > p {
line-height: 1.8;
}
/* 标题后的第一段字号略小、颜色偏灰 */
h2 + p {
font-size: 0.95em;
color: #666;
}
/* 列表中除第一项外都加上分隔线 */
li ~ li {
border-top: 1px solid #eee;
}把这些关系选择器与前面的交集、并集混合使用,可以构建出非常精确的匹配条件。例如ul.menu > li.active + li表示选中菜单中active项的下一个兄弟项,常见于去掉选中项右侧的边框线这类细节处理。写这类复杂选择器时建议从左到右逐层确认,先确定起点,再看关系符号,最后确认目标,条理清晰了就不容易写错。
组合选择器的优先级与维护性考量
选择器一旦组合,特异性就会随之变化,这是很多样式冲突问题的根源。计算规则可以简单概括为:ID选择器记为a位,类选择器、属性选择器、伪类各记为b位,标签选择器和伪元素记为c位,然后把三个数字拼在一起比较。例如#nav .item:hover的特异性是1、2、0,明显高于.item:hover的0、2、0。组合的条件越多,特异性越高,后续想用一条简单规则覆盖它就越困难,最终往往被迫用!important来强行压制,形成恶性循环。
工程上的建议是控制组合的深度和长度。能靠单一class解决的,就不要写成多层嵌套;避免在选择器中出现ID,因为它会让特异性直接上一个台阶。现代CSS方法论如BEM,本质上就是通过命名约定把复杂的层级关系"扁平化"成单个class,从根本上绕开了特异性大战。当然,结构类选择器如:nth-child()、状态类如:checked仍然是不可替代的,合理地将它们与类选择器组合,才是平衡精确性与可维护性的正确做法。
调试时可以借助浏览器开发者工具,在Elements面板中查看某个元素被哪些规则命中,Styles面板会列出所有匹配的选择器及其优先级排序,被划掉的规则就是被更高特异性覆盖的。遇到样式不生效的情况,先检查选择器是否真的匹配到了目标元素,再检查特异性是否被别的规则压制,按这个顺序排查,绝大多数问题都能快速定位。