普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月30日首页

CSS 技巧:CSS 中选择 html 元素的各种奇技淫巧

2026年3月30日 12:19

在 CSS 中选中 <html> 元素,这件事看起来再基础不过。大多数情况下,我们只需要写下 html {} 或者 :root {},问题就已经解决了,而且这也是最推荐、最常见的做法。

但如果稍微换个角度去想,除了这些“标准答案”,有没有其他方式也能选中 <html>?答案是——有,而且还不少。

当然,这些写法在实际项目中几乎没有使用价值,甚至可以说有点“多此一举”。不过,它们有一个很有意思的意义——可以帮助我们更深入地理解 CSS 选择器的工作原理。当你开始思考这些问题时,比如 :scope 在什么情况下等价于根元素、& 在非嵌套环境下到底代表谁、:has() 是否可以“反向”匹配父级,甚至能不能选中“没有父元素”的节点,你会发现 CSS 的灵活性远比想象中更高。

所以,这篇内容并不是在教你最佳实践,而更像是一场轻松的探索。我们会用各种“非常规”的方式去选中 <html>,看看 CSS 选择器的边界到底在哪里——以及,它到底能被玩到多离谱。

html:root

刚才提到过,通常我们会使用经典且熟知的 html {}:root{} 来选中 <html> 元素:

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,高于元素选择器 html0-0-1。这意味着在样式冲突时,使用 :root 定义的规则更容易生效,从而减少被其他样式覆盖的可能性。

&:scope

接下来,我们来看一些你可能不太熟知的方法。我们可以先从最短、也是最“奇怪”的选择器开始——嵌套选择器 & 。它只有一个字符,但在特定情况下却可以直接选中 <html>

& {
    background-color: lightblue;
}

接下来是 :scope 选择器:

:scope {
    background-color: lightblue;
}

这两个写法之所以都能“指向” <html> ,其实依赖的是它们的回退行为。当 & 没有出现在嵌套规则中时,它不会再“拼接父选择器”,而是退化为指向当前作用域的根;而在没有显示式定义作用域(例如没有使用 @scope)的情况下,:scope 也会表示文档的根节点。于是,在普通的 HTML 文档中,它们最终都会指向 <html>

不过,从设计初衷来看,:scope& 的用途其实完全不同。:scope 用来表示“当前作用域的根元素”,而这个“根”在使用 @scope 时是可以被重新定义的;只有在默认情况下,它才等同于 <html> 。而 & 则主要用于 CSS 嵌套,用来引用当前选择器本身,从而实现更直观的嵌套写法。

例如:

element:hover {
    /* 写法一 */
}

element {
    &:hover {
        /* 等价于上面(注意 &) */
    }
}

如果省略 &,语义就会发生变化:

element {
    :hover {
        /* 实际变成 element :hover(注意空格) */
    }
}

甚至还可以写出更“绕”的形式:

element {
    :hover & {
        /* 表示 :hover element */
    }
}

但一旦 & 脱离了嵌套环境,它就不再参与选择器拼接,而只是简单地指向作用域根。在没有 @scope 的情况下,这个根就是 <html>——这也是它成为一个“隐藏选择器”的原因之一。

温馨提示:如果你对 CSS 的嵌套与作用域机制感兴趣,尤其是 &@scope 的用法,可以进一步阅读《CSS 的嵌套和作用域:&@scope》,会有更深入的理解。

:has(head):has(body)

我们还可以借助 :has() 这个“反向选择器”来选中 <html> 。例如:

: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> 是页面中唯一没有父元素的节点。基于这一点,可以写出一个略显“花哨”的选择器:

:not(* *) {
    background-color: lightblue;
}

这里的 * * 表示“所有被其他元素包含的元素”,而 :not(* *) 就是把这些元素全部排除掉。最终剩下的,正是那个不被任何元素包含的 <html>。顺便一提,* 被称为“通配选择器”,可以匹配任意元素。

你也可以在中间加入子代组合符 >

:not(* > *) {
    background-color: lightblue;
}

当然,围绕这些思路,我们还可以继续组合出更多“奇技淫巧”的写法,例如:

:is(&) {}
:where(&) {}
&& {}
&&&& {} /* 没错,& 可以无限叠加 */
:has(> body)
:has(> head)
:has(body, head)
/* 等等... */

这些写法有实际价值吗?大多数情况下并没有。但作为一次探索 CSS 选择器能力边界的练习,它们既有趣,也能帮助你更深入地理解选择器背后的机制。

小结

到这里,我们用各种“非常规”的方式,把 <html> 元素从头到尾“折腾”了一遍。从最常见的 html:root,到利用回退行为的 :scope&,再到借助结构关系的 :has(),甚至是通过“排除一切”的 :not(),你会发现:选中 <html> 的方法,远比想象中要多

但更重要的并不是这些写法本身,而是它们背后所体现的规则——选择器的匹配逻辑、作用域的概念、优先级的影响,以及 CSS 在不同上下文中的行为方式。这些才是真正值得理解的部分。

当然,在实际项目中,我们依然应该优先使用简单、清晰、可维护的写法,比如 html:root。那些“奇技淫巧”更多是一种探索和练习,它们的价值在于帮助你建立更扎实的底层认知,而不是直接拿来用在生产环境中。

如果说这篇内容有什么收获,那大概就是:CSS 远不只是“写样式”这么简单,它本身也是一门可以被不断挖掘和玩出花样的语言

昨天以前首页

为什么 :is(::before, ::after) 不能工作?

2026年3月20日 08:57

在 CSS 中,:is() 是一个非常实用的函数型伪类,它可以帮助我们简化和合并选择器。很多 Web 开发者都会用它来减少重复代码,让选择器更简洁、更易读。

例如:

button.large,
button.small {
    /* CSS */
}

可以使用 :is() 改写为:

button:is(.large, .small) {
    /* CSS */
}

这样既保持了相同的效果,又让代码更加紧凑。因此,很多人会把 :is() 理解为一种 “合并选择器”的工具

不过,当你对 :is() 越来越熟悉时,可能会产生一个看起来很合理的想法:既然 :is() 可以合并选择器,那是否也可以用来同时选择元素的伪元素?例如:

button:is(::before, ::after) {
    /* CSS */
}

从直觉上看,这似乎是在说:选择 button ::before ::after。但实际上,这段代码不会生效。浏览器会直接忽略它,因为在 CSS 规范中,伪元素是不允许写在 :is() 里的

这就引出了一个非常有意思的问题:为什么 :is(::before, ::after) 不能工作? 要理解这个问题,我们需要先弄清楚一件事: :is() 在 CSS 选择器中到底是如何工作的。 很多开发者对它的理解,其实和浏览器真正的解析方式并不完全一样。

在接下来的内容中,我们会一步一步拆解:

  • :is() 的真实作用是什么

  • 为什么伪元素不能出现在 :is()

  • 为什么 :is(:hover, :focus) 是合法的,而 :is(::before, ::after) 却不是

  • 以及如何正确地编写涉及伪元素的选择器

理解这些规则之后,你不仅能避免一个常见的 CSS 坑,还能更准确地理解 CSS 选择器的工作方式

:is() 的真实作用是什么?

很多人第一次接触 :is() 时,都会把它理解为一种用来合并选择器的语法糖。例如:

button.large,
button.small {
    /* CSS */
}

可以写成:

button:is(.large, .small) {
    /* CSS */
}

从结果上看,两种写法确实是等价的,因此很容易让人产生一个印象: :is() 的作用就是把多个选择器合并成一个。但实际上,这只是表面现象。

在 CSS 选择器中,:is() 的真实作用并不是“展开选择器”,而是为当前元素增加匹配条件。换句话说,:is() 本身是一个伪类(pseudo-class) 。它的行为和 .class#id:hover 这些选择器类似,都是在为元素增加匹配规则。

例如:

button:is(.large, .small) {
    /* CSS */
}

正确的理解方式是:选择所有 既是 button 元素,同时又匹配 :is() 内部条件的元素。也就是:

  • 元素必须是 button

  • 同时具有 .large .small

因此,它并不是先被浏览器转换成:

button.large,
button.small {
    /* CSS */
}

再去匹配元素,而是浏览器直接按照选择器规则进行匹配判断

理解这一点非常重要,因为它会影响我们如何阅读选择器。例如:

:is(.card, .panel) {
    /* CSS */
}

表示:选择任何 匹配 .card .panel 的元素。 而如果写成:

button:is(.card, .panel) {
    /* CSS */
}

含义就变成:选择 既是 button 元素,同时又匹配 .card .panel 的元素。

因此可以总结为一句话: :is() 用来为当前元素增加一组“可选的匹配条件”。 它并不会改变选择器的目标,也不会去“选择”括号里的内容,而只是判断当前元素是否满足其中任意一个条件

正是因为这个机制,才会导致一个容易让人困惑的现象:伪元素(例如 ::before ::after )不能写在 :is() 里面。

为什么伪元素不能出现在 :is() 中?

理解这个问题的关键,仍然是 :is() 的作用。正如前面提到的,:is() 是一个伪类,它只是为当前元素增加匹配条件。也就是说,它是在判断:当前元素是否满足括号中的某个选择器

例如:

button:is(.large, .small) {
    /* CSS */
}

意思是:选择既是 button 元素,同时又匹配 .large.small 的元素。换句话说,:is() 只是用来判断 button 元素是否符合这些条件

现在再来看这个选择器:

button:is(::before, ::after) {
    /* CSS */
}

如果按照同样的逻辑去阅读,它的含义就变成:选择既是 button 元素,同时又匹配 ::before ::after 的元素

问题就在这里,元素不可能是伪元素:

  • button 是一个 真实的 DOM 元素

  • ::before::after伪元素

伪元素并不是独立的节点,而是附属于某个元素生成的内容。因此,一个元素不可能同时既是元素又是伪元素。所以这个选择器的条件永远不可能成立,浏览器也就不会匹配到任何内容。

这也是为什么 :is() 中只允许写普通选择器或伪类,而不能写伪元素。例如下面的写法是完全合法的:

button:is(:hover, :focus, :active){
    /* CSS */
}

它的意思是:选择所有 处于 :hover :focus :active 状态的 button 元素。因为 :hover:focus:active 都是伪类,只是描述元素状态,因此可以作为匹配条件使用。

简单来说,可以记住这样一条规则: :is() 用来给元素增加匹配条件,而不是用来选择伪元素。 同样的限制也适用于另外两个伪类 :not():where()

温馨提示:有关于 :is():not():where() 选择更详细的介绍,请移步阅读《CSS 选择器::where() vs. :is()》!

如何正确地编写涉及伪元素的选择器?

既然 :is() 不能包含伪元素,那么当我们需要同时为多个伪元素编写样式时,应该怎么写?最常见、也是最推荐的方法,就是使用逗号分隔的选择器列表

例如:

button::before,
button::after {
    content: "";
    position: absolute;
}

这种写法虽然稍微长一点,但它清晰、直观,而且完全符合 CSS 规范。

如果多个选择器的主体部分相同,而只有伪元素不同,也通常只能这样写:

.card::before,
.card::after {
    content: "";
    position: absolute;
}

这里实际上是在选择.card::before.card::after。而不是试图把伪元素放进 :is() 中。

不过,我们仍然可以在元素部分使用 :is() ,然后在最后添加伪元素。例如:

:is(button, a)::before {
    content: "";
}

这个选择器的意思是:选择 buttona 元素的 ::before 伪元素。同样的思路也可以用于类选择器:

:is(.card, .panel)::after {
    content: "";
}

表示 .card::after.panel::after

因此,可以记住一个简单的经验法则: :is() 可以用来匹配元素,但伪元素必须写在选择器的最后。 例如:

/* ✅ 正确写法 */
:is(button, a)::before {
    /* CSS */
}

/* ❌ 错误写法 */
button:is(::before, ::after) {
    /* CSS */
}

总结

:is() 是一个非常强大的 CSS 工具,它可以帮助我们减少重复选择器,让代码更简洁。但需要记住两点关键规则:

  • :is() 用于匹配元素条件,而不是选择伪元素

  • 伪元素必须始终写在选择器的最后

理解这一点之后,你不仅可以避免 :is(::before, ::after) 这样的常见错误,还能更深入地理解 CSS 选择器的匹配机制

❌
❌