
在 CSS 中选中 <html> 元素,这件事看起来再基础不过。大多数情况下,我们只需要写下 html {} 或者 :root {},问题就已经解决了,而且这也是最推荐、最常见的做法。
但如果稍微换个角度去想,除了这些"标准答案",有没有其他方式也能选中 <html>?答案是------有,而且还不少。
当然,这些写法在实际项目中几乎没有使用价值,甚至可以说有点"多此一举"。不过,它们有一个很有意思的意义------可以帮助我们更深入地理解 CSS 选择器的工作原理 。当你开始思考这些问题时,比如 :scope 在什么情况下等价于根元素、& 在非嵌套环境下到底代表谁、:has() 是否可以"反向"匹配父级,甚至能不能选中"没有父元素"的节点,你会发现 CSS 的灵活性远比想象中更高。
所以,这篇内容并不是在教你最佳实践,而更像是一场轻松的探索。我们会用各种"非常规"的方式去选中 <html>,看看 CSS 选择器的边界到底在哪里------以及,它到底能被玩到多离谱。
html 和 :root
刚才提到过,通常我们会使用经典且熟知的 html {} 和 :root{} 来选中 <html> 元素:
CSS
html {
background-color: lightblue;
}
/* 或者 */
:root {
background-color: lightblue;
}
在大多数情况下,html 和 :root 的效果是一样的,但它们本质上属于两种不同类型的选择器。它们之间的差异主要体现在语义、适用范围和优先级上。
从本质来看,html 是一个元素选择器,它的作用非常直接,就是选中页面中的 <html> 标签本身;而 :root 则是一个伪类选择器,它匹配的是"文档的根元素"。在 HTML 文档中,这个根元素恰好就是 <html> 元素,因此两者在这里表现一致。
不过,这种一致只是"刚好如此"。从语义角度来说,html 表达的是一个具体的标签,而 :root 表达的是一种结构上的位置------也就是最顶层的那个元素。这种差别在其他类型的文档中就会变得明显:
-
HTML 文档:
:root匹配<html> -
SVG 文档:
:root匹配<svg> -
RSS 文档:
:root匹配<rss> -
Atom 文档:
:root匹配<feed> -
MathML 文档:
:root匹配<math> -
其他 XML 文档:
:root匹配最外层元素,比如<note>
那 :root 的实际意义是什么呢?一个很关键的点在于它的优先级。作为伪类选择器,:root 的权重是 0-1-0 ,高于元素选择器 html 的 0-0-1 。这意味着在样式冲突时,使用 :root 定义的规则更容易生效,从而减少被其他样式覆盖的可能性。
& 和 :scope
接下来,我们来看一些你可能不太熟知的方法。我们可以先从最短、也是最"奇怪"的选择器开始------嵌套选择器 & 。它只有一个字符,但在特定情况下却可以直接选中 <html> :
CSS
& {
background-color: lightblue;
}
接下来是 :scope 选择器:
CSS
:scope {
background-color: lightblue;
}
这两个写法之所以都能"指向" <html> ,其实依赖的是它们的回退行为 。当 & 没有出现在嵌套规则中时,它不会再"拼接父选择器",而是退化为指向当前作用域的根;而在没有显示式定义作用域(例如没有使用 @scope)的情况下,:scope 也会表示文档的根节点。于是,在普通的 HTML 文档中,它们最终都会指向 <html> 。
不过,从设计初衷来看,:scope 和 & 的用途其实完全不同。:scope 用来表示"当前作用域的根元素",而这个"根"在使用 @scope 时是可以被重新定义的;只有在默认情况下,它才等同于 <html> 。而 & 则主要用于 CSS 嵌套,用来引用当前选择器本身,从而实现更直观的嵌套写法。
例如:
CSS
element:hover {
/* 写法一 */
}
element {
&:hover {
/* 等价于上面(注意 &) */
}
}
如果省略 &,语义就会发生变化:
CSS
element {
:hover {
/* 实际变成 element :hover(注意空格) */
}
}
甚至还可以写出更"绕"的形式:
CSS
element {
:hover & {
/* 表示 :hover element */
}
}
但一旦 & 脱离了嵌套环境,它就不再参与选择器拼接,而只是简单地指向作用域根。在没有 @scope 的情况下,这个根就是 <html>------这也是它成为一个"隐藏选择器"的原因之一。
温馨提示 :如果你对 CSS 的嵌套与作用域机制感兴趣,尤其是
&和@scope的用法,可以进一步阅读《CSS 的嵌套和作用域:&和@scope》,会有更深入的理解。
:has(head) 和 :has(body)
我们还可以借助 :has() 这个"反向选择器"来选中 <html> 。例如:
CSS
:has(head) {
background-color: lightblue;
}
/* 或者 */
:has(body) {
background-color: lightblue;
}
之所以可行,是因为在规范上,<html> 元素只应该包含 <head> 和 <body> 这两个直接子元素(有点像那种"非黑即白"的设定)。如果你在 <html> 里写入其他标签,那属于无效 HTML,虽然浏览器通常会"帮你收拾残局",把这些元素自动移动到 <head> 或 <body> 中。
更关键的一点是,在标准结构中,没有其他元素可以包含 <head> 或 <body> 。因此,当我们写 :has(head) 或 :has(body) 时,理论上只会匹配到 <html> 元素本身(除非你刻意写出错误的嵌套结构,但那显然不是正常用法)。
这种方式实用吗?其实并不太实现。但它很好地展示了 :has() 的能力,同时也顺带帮你复习了一下什么才是"合法的 HTML 结构"。
温馨提示 :如今,:has() 选择器为 CSS 带来了前所未有的能力,它让我们可以完成许多过去必须依赖 JavaScript 才能实现的效果。如果你对这些更进阶的用法感兴趣,那么下面这几节课的内容非常值得花时间深入了解。
:not(* *)
除了前面那些方法,我们还可以利用一个很有意思的事实: <html> 是页面中唯一没有父元素的节点。基于这一点,可以写出一个略显"花哨"的选择器:
CSS
:not(* *) {
background-color: lightblue;
}
这里的 * * 表示"所有被其他元素包含的元素",而 :not(* *) 就是把这些元素全部排除掉。最终剩下的,正是那个不被任何元素包含的 <html>。顺便一提,* 被称为"通配选择器",可以匹配任意元素。
你也可以在中间加入子代组合符 > :
CSS
:not(* > *) {
background-color: lightblue;
}
当然,围绕这些思路,我们还可以继续组合出更多"奇技淫巧"的写法,例如:
CSS
:is(&) {}
:where(&) {}
&& {}
&&&& {} /* 没错,& 可以无限叠加 */
:has(> body)
:has(> head)
:has(body, head)
/* 等等... */
这些写法有实际价值吗?大多数情况下并没有。但作为一次探索 CSS 选择器能力边界的练习,它们既有趣,也能帮助你更深入地理解选择器背后的机制。
小结
到这里,我们用各种"非常规"的方式,把 <html> 元素从头到尾"折腾"了一遍。从最常见的 html 和 :root,到利用回退行为的 :scope 和 &,再到借助结构关系的 :has(),甚至是通过"排除一切"的 :not(),你会发现:选中 <html> 的方法,远比想象中要多。
但更重要的并不是这些写法本身,而是它们背后所体现的规则------选择器的匹配逻辑、作用域的概念、优先级的影响,以及 CSS 在不同上下文中的行为方式。这些才是真正值得理解的部分。
当然,在实际项目中,我们依然应该优先使用简单、清晰、可维护的写法,比如 html 或 :root。那些"奇技淫巧"更多是一种探索和练习,它们的价值在于帮助你建立更扎实的底层认知,而不是直接拿来用在生产环境中。
如果说这篇内容有什么收获,那大概就是:CSS 远不只是"写样式"这么简单,它本身也是一门可以被不断挖掘和玩出花样的语言。