网页开发时总有些基础到容易被忽略的细节,比如样式表最开头那个:root和html到底啥关系?还有一堆看着像脑筋急转弯的选择器,:has(head)这种写法到底图个啥?这次就扒一扒这些藏在角落里的CSS选择器,看看它们是真没用,还是藏着点意外之喜。
摘要
CSS里有些选择器写法看着像代码整活,比如:not(* *)或者:has(body),但它们背后其实跟文档结构绑定得死死的。:root在不同XML文档里指代不同根元素,而&符号在嵌套之外也能单独干点别的事。这篇不整虚的,直接上手把几个容易搞混的选择器拎出来实操一遍,看看从:scope到鸟形选择器都能玩出什么花样,顺带避一避那些一不留神就踩坑的写法。
:root 与 html 的较劲
平时写全局CSS变量,十有八九都往:root里塞,但:root和html在HTML文档里确实指向同一个元素。不过:root这玩意儿出生就不是只给HTML准备的,碰到SVG文档的时候,:root就变成<svg>标签;碰到RSS文档,又变成<rss>。在Atom订阅文档里它又是<feed>,MathML里则是<math>。这就像一个人在不同场合得换不同身份,虽然本尊还是那个根元素,但叫法得跟着文档类型变。
:root的优先级比html高,伪类选择器的权重是0-1-0,元素选择器只有0-0-1。写样式的时候要是发现:root和html同时定义了相同属性,:root说了算。比如想给整个页面搞点全局颜色变量,直接这样操作:
:root {
--main-bg: #f0f0f0;
--text-color: #333;
}
html {
background-color: var(--main-bg);
color: var(--text-color);
}全局变量放:root上其实没啥毛病,但要是较真一点,:scope在语义上更贴合全局范围的概念,因为:scope就是瞄着当前作用域的根去的。不过实际跑起来,:root和:scope在全局作用域里效果一模一样,连浏览器渲染出的结果都没差别。
:scope 与 & 的变脸
:scope这货在全局用的时候,它就是<html>的另一个马甲。但一旦钻到@scope规则里头,它就彻底变脸了,变成当前作用域指定的根元素。比如定义一个作用域:
@scope (.card) {
:scope {
border: 1px solid #ccc;
}
p {
margin: 0;
}
}:scope在这里就是.card本身,而不是<html>。这样样式就只在.card内部起作用,不会污染外面。
而&符号在CSS嵌套里常见的用法是把父选择器拼过来,比如:
.button {
background: blue;
&:hover {
background: darkblue;
}
}&在这里直接替换成.button,得到.button:hover。但&这玩意儿不嵌套的时候,它自己单独放在一个样式块里,也会指向作用域的根元素。在全局样式文件里,单独写个&块,跟直接写:root或者html是一个意思:
& {
font-family: sans-serif;
}这在平时写样式的时候可能用不上,但要是搞点样式库的混入逻辑,或者动态生成样式片段,&的这种特性就能派上用场。
:has(head) 与 :has(body) 的精准狙杀
:has(head)和:has(body)这俩选择器在HTML文档里几乎就是冲着<html>标签去的。因为HTML规范里,<html>的直接子元素只能是<head>和<body>,其他元素都没这资格。所以只要一个元素包含<head>或者<body>,那它必然就是<html>。
这么写虽然有点绕,但可以用来给<html>单独加样式,而且不用写具体的元素名。比如页面里要是有人手贱在<body>里又塞了个<body>,:has(body)也能精准逮到那个不合法的外层<html>。不过正常开发里,这用法纯属炫技,因为直接html选择器比这清晰多了。
但:has()这个伪类本身是很有用的,像筛选包含某个子元素的父元素,:has()就是神器。比如只想让包含图片的段落有特殊样式:
p:has(img) {
border: 2px solid red;
padding: 10px;
}这才是:has()的正确打开方式,前面拿:has(head)举例纯粹是为了展示选择器的匹配逻辑有多死板。
:not(* *) 与鸟形选择器的逻辑游戏
:not(* *)这写法看着像在玩文字游戏。* *的意思是有祖先元素的任意元素,也就是除了根元素以外的所有元素。前面再套个:not(),就变成了“不是有祖先元素的元素”,那剩下的就只有根元素了。所以:not(* *)实际上就是<html>的另一种写法。
更有意思的是把子组合器塞进去,写成:not(* > *),这里* > *表示所有有直接父元素的元素,一样是除了根元素之外的所有元素。前面加个:not(),最终匹配的还是根元素。但要是写成:not(* > *)不带空格,这玩意儿看着就有点像鸟的侧面,网上有人管它叫“鸟形选择器”。
这玩意儿在实际项目里基本用不上,但它能帮着理解选择器组合的逻辑。比如排查样式覆盖问题的时候,这种复杂选择器可以用来测试优先级和匹配范围。更实用的反而是:not()配合其他简单选择器,比如只给不是第一项的元素加边框:
li:not(:first-child) {
border-top: 1px solid #eee;
}这种写法比li + li更直白,可读性也更好。
从根到鸟的实操路径
遇到需要精准控制全局样式但又不想跟现有html样式起冲突的场景,可以直接上:root搭配:where()来降低优先级,这样既能覆盖又不会太强势:
:root:where(html) {
scroll-behavior: smooth;
}:where()的优先级是0-0-0,这样全局滚动行为就不会把其他地方的滚动样式给压死。
如果想在组件库内部限制样式范围,但又不想用笨重的@scope(兼容性问题),可以用:is()配合:not()做个伪作用域:
.component :is(div, p, span) {
color: inherit;
}
.component :not(:is(div, p, span)) {
margin: 0;
}这种写法虽然不如真正的@scope干净,但在老项目里临时隔离样式挺好使。
至于:has(),想给所有包含视频的板块加个暗色背景,直接:
section:has(video) {
background: #1a1a1a;
padding: 1rem;
}代码读起来就跟人说话似的,“包含视频的section”,比写JS去遍历查找清爽多了。
那些看着没用的选择器,像是:not(* *)或者:has(body),虽然平时用不上,但它们反映了CSS选择器匹配机制的底层逻辑。把这些冷门知识摸透之后,再遇到复杂的样式覆盖或者需要写高度可维护的选择器时,就能从原理层面避开坑,而不是靠瞎试。
