• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • 现代CSS

    • 01.CSS的父选择器::has()
    • 02.CSS选择器:has()能解决什么问题
    • 03.CSS选择器::has()与:not()的组合
    • 04.CSS选择器::where()vs.:is()
    • 05.CSS焦点样式::focus-visible和:focus-within
    • 06.用于美化模态框的:modal和::backdrop
    • 07.CSSCustomHighlightAPI:Web文本范围高亮的未来
    • 08.画中画:CSS的:picture-in-picture伪类
    • 09.CSS显式默认值:inherit,initial,unset和revert
    • 10.现代CSS中的颜色格式:RGB,HSL,HWB,LAB和LCH
    • 11.新的CSS颜色空间:为Web设置高清颜色
    • 12.CSS中的OKLCH和OKLAB
    • 13.CSS的混合颜色:color-mix()
    • 14.Web控件UI颜色的定制:accent-color和color-scheme
    • 15.颜色对比度:color-contrast()
    • 16.CSS的比较函数:min(),max()和clamp()
    • 17.CSS的三角函数
    • 18.F-mods:可用于@font-face的新特性
    • 19.CSS的text-box-trim和text-box-edge给排版带来的变化
    • 20.Web上的可变字体
    • 21.Web上的彩色字体
    • 22.首字母下沉:initial-letter
    • 23.经典排版技术:使用text-wrap:balance实现文本平衡换行
    • 24.CSS自定义属性你知道多少
    • 25.CSS自定义属性可以用来做些什么
    • 26.现代CSS中的相对单位
    • 27.CSS逻辑属性和逻辑值
    • 28.CSSGrid之瀑布流布局:masonry和masonry-auto-flow
    • 29.CSS媒体查询新特性:@media
    • 30.CSS判断:@supports
    • 31.CSS分层:@layer
    • 32.CSS容器查询之尺寸查询
    • 33.CSS容器查询之样式查询和状态查询
    • 34.CSS的嵌套和作用域:&和@scope
    • 35.CSS计数器:@counter-style
    • 36.CSS自定义属性:@property
    • 37.CSS变换之单个变换
    • 38.CSS宽高比:aspect-ratio
    • 39.CSS的锥形渐变
    • 40.CSS的Clipping和Masking
    • 41.CSS的object-view-box
    • 42.CSS图像处理与特效指南
    • 43.CSS滚动驱动动效
    • 44.CSS路径动画
    • 45.CSS动画合成:animation-composition
    • 46.解锁CSSViewTransitionsAPI的魔力
    • 47.CSS锚点定位:探索下一代Web布局

近几年中,CSS 为 Web 开发者提供了很多优秀且强大的选择器,可以很好地帮助 Web 开发者快速选中目标元素。有些新选择器还可以允许你在开发的过程中减少对类名和 JavaScript 的依赖,比如, :has() 和 :not() 选择器。

只不过,CSS 中依然缺失很多强大的选择器,比如通过选择器来选择一个范围内的元素。庆幸的是,CSS 的 :has() 和 :not() 选择器组合在一起,可以构建出更多强大的选择器,比如 :nth-child(An+B [of S]?) 。在这节课中,我们将一起来探讨 :has() 与 :not() 组合在一起可以做哪些事情?又有哪些差异和需要注意的细节?

:has() 和 :not() 是什么?

CSS 的 :has() 和 :not() 选择器都被称为函数伪类,其中 :has() 被称为是一个关系选择器。它可以基于包含特定后代来匹配祖先元素,但它也可以基于后续的内容匹配前置元素等。比如:

figure:has(> figcaption) {
    /* 选中含有子元素 figcaption 的 figure */
}

代码中的选择器,将会选中含有子元素 figcaption 的 figure 元素,并且样式规则会运用于 figure 。

有关于 :has() 是什么以及相关详细介绍,请移步阅读《CSS 的父选择器::has()》!

而 :not() 被称为是一个否定伪类选择器,它可以用来作为条件判断,类似 JavaScript 中的非。其主要作用就是防止特定的元素被选中,因此它也被称为反选伪类。比如:

/* 选择 body 后代元素中不是段落 p 的元素 */
body :not(p) {
    color: #000;
}

/* 选择没有 .fancy 类名的段落 p 元素 */
p:not(.fancy) {
    color: orange;
}

/* 选择 body 后代元素中不是 p 或 span 元素 */
body :not(p):not(span) {
    color: red;
}

/* 选择 body 后代元素中不是 p 或 span 元素 */
body :not(p, span) {
    color: yellow;
}

:has() 和 :not() 可以做什么?

我们在小册第二节课《CSS 选择器 :has() 能解决什么问题?》中花了很大的篇幅阐述了 :has() 选择器可以做什么。因此,这节课我们来看看 :not() 选择器可以做什么?

我们平时开发项目的时候,时常会碰到列表这样的效果,列表项之间有一个 margin-bottom,而往往想在最后一项中不需要设置 margin-bottom。比如下图这样的效果:

img

正如上图所示,如果我们每一个 .card 都指定一个 margin-bottom 值,就会出现上图中左侧所呈现的效果,无法达到 Web 设计师预期的效果。这个时候,我们使用 :not() 伪类选择器就可以很好地避免这个现象:

.cards {
    padding: 20px;
}

.card:not(:first-child) {
    margin-top: 20px;
}

/* 或者 */
.card:not(:last-child) {
    margin-bottom: 20px;
}

上面代码意思很简单:

  • .card:not(:first-child) 表示除了第一个 .card 之外的所有 .card 都指定 margin-top 的值为 20px;
  • .card:not(:last-child) 则表示除了最后一个 .card 之外的所有 .card 都指定 margin-bottom 的值为 20px。

img

Demo 地址:https://codepen.io/airen/full/poxeKrB

现在,:not() 伪类在 CSS 中支持复杂选择器。

复杂选择器是由组合符分隔的一个或多个复合选择器的序列。

在 :not() 伪类中支持复杂选择器的有趣之处在于,现在可以使用通用选择器(*)选择不是其他元素的子级或后代元素。比如下面这个示例:

<img
    src="./logos.png"
    alt="A Twitter, RSS and Twitch logo"
    width="508"
    height="201" />

<picture>
    <source srcset="./logos.webp" type="image/webp" />
    <img
        src="./logos.png"
        alt="A Twitter, RSS and Twitch logo"
        width="508"
        height="201" />
</picture>

如今,你可以使用 :not() 选择器选择所 img 元素,但它不是 picture 元素的子元素:

img:not(picture *) {
    border: 3px solid red;
}

img

Demo 地址:https://codepen.io/airen/full/jOeBKdj

就此例而言,我们也可以使用简单的 :not() 选择器达到相同的效果:

img

Demo 地址:https://codepen.io/airen/full/mdzWKoK

上面示例告诉我们,CSS 的 :not() 选择器的使用并不复杂,但你在使用它的时候,有几点还需要注意:

  • 可以使用 :not() 选择器编写一个完全无用的选择器。例如,:not(*) 匹配任何不是元素的元素,这显然是荒谬的,所以这个附加的规则将永远不被应用。
  • 可以利用 :not() 选择器提高选择器的权重。例如,#foo:not(#bar) 和 #foo 都将匹配相同的元素,但是具有两个 id 的选择器的权重要更高(#foo:not(#bar) 的权重是 (3, 0,1) ,#foo 的权重是 (1,0,0))。
  • :not() 伪类的优先级将由其逗号分割的参数中优先级最高的选择器指定;提供与 :not(:is(argument)) 相同的优先级。
  • :not(.foo) 将匹配任何非 .foo 的元素,包括 <html> 和 <body>。
  • :not() 选择器将匹配任意“不是一个 X”的元素。当与后代选择器一起使用,这可能令人惊讶,因为有多种路径可以选择一个目标元素。例如,body :not(table) a 仍将应用 <table> 中的 a 元素,因为 <tr>、<tbody>、<th>、<td>、<caption> 等都可以匹配选择器 :not(table) 部分。
  • 你可以同时否定多个选择器。例如::not(.foo, .bar) 等同于 :not(.foo):not(.bar)。
  • :not() 选择器和 :has() 选择器一样,都被视为严格型选择器。如果传递给 :not() 伪类的选择器无效或者浏览器不支持,则整个规则都将是无效的。克服这种行为的有效方式是使用::is() 或 :where() 伪类,它们接受一个可容错选择器列表。例如 :not(.foo, :invalid-pseudo-class) 将使整个规则无效,但是 :is(:not(.foo), :not(:invalid-pseudo-class)) 或 :where(:not(.foo), :not(:invalid-pseudo-class)) 将匹配任何不是 .foo 的元素。

可以将 :has() 和 :not() 组合在一起使用

我们在实际使用的时候,可以将 :has() 和 :not() 两个选择器组合在一起。比如:

a:not(:has(> svg)) {
    color: red;
}

a:not(:has > svg) 选择器将会选择所有 <a> 元素,但是它们不能直接包含 <svg> 元素,即 <a> 元素中不能直接有子元素 <svg> ,但可以有后代的 <svg> 元素。因为 :has() 选择器指定了要包含一个子元素 <svg> ,而不是更深层的后代元素:

<!-- 相匹配的 HTML 结构 -->
<a href="">CSS Selector</a>

<a href="">
    CSS Selector
    <span><svg></svg></span>
</a>

<!-- 不相匹配的 HTML 结构 -->
<a href="">CSS Selector <svg></svg></a>

你也可以在 :has() 中包含 :not() ,比如:

article:has(img:not([alt])) {
    color: red;
}

article:has(img:not([alt])) 选择器会选中所有包含 <img> ,且这个 <img> 元素没有 alt 属性的 article 元素。具体来说,:has() 选择器指定了 article 元素必须包含一个满足条件的子元素或后代元素,即 img:not([alt]) ,也就是含有 <img> 元素,且没有 alt 属性。如果条件满足,那么这个 article 元素就被选中。

<!-- 相匹配的 HTML 结构 -->
<article>
    Article Element
    <img src="article.jpg" />
    <!-- img 是 article 的子元素,且没有设置 alt 属性 -->
</article>

<article>
    Article Element
    <figure>
        <img src="figure.jpg" />
        <!-- img 是 article 的后代元素,且没有设置 alt 属性 -->
    </figure>
</article>

<!-- 不相匹配的 HTML 结构 -->
<article>
    Article Element
    <img
        src="article.jpg"
        alt="article element" /><!-- img 是 article 的子元素,但设置了 alt 属性 -->
</article>

<article>
    Article Element
    <figure>
        <img
            src="figure.jpg"
            alt /><!-- img 是 article 的后代元素,但设置了 alt 属性 -->
    </figure>
</article>

它们的结合看上去没什么,但对于初学者而言,有的时候总是易于混淆,尤其是 :has(:not()) 与 :not(:has()) 之间的差异。让我通过一个示例来展示它们之间的差异。假设我们有两张卡片,每张卡片都有一个标题和一些描述文本,但其中一张卡片没有缩略图。它们对应的 HTML 结构如下:

<!-- 包含缩略图的卡片 -->
<div class="card">
    <figure>
        <img src="card-thumbnail.jpg" alt="Card Thumbnail" />
    </figure>
    <h3>Card Title</h3>
    <p>Card Description</p>
</div>

<!-- 不包含缩略图的卡片 -->
<div class="card">
    <h3>Card Title</h3>
    <p>Card Description</p>
</div>

现在,我们想要为没有包含图片的卡片设置一个内距(padding):

.card:has(:not(img)) {
    padding: 1rem;
}

img

Demo 地址:https://codepen.io/airen/full/vYVxzJO

正如你所看到的,两张卡片都设置了 padding: 1rem ,无论卡片是否包含了缩略图 img 。这是因为 .card:has(:not(img)) 的意思是“选择一个包含任何非 img 元素的 .card”。这意味着该选择器仅在卡片仅包含 img 时不适用。

说实话,最开始也令我感到意外,我原本对 .card:has(:not(img)) 理解是,不包含 img 元素的 .card 元素,但事实上却事与愿违。

如果我们把 :has() 和 :not() 选择器的嵌套关系对换一下,例如:

.card:not(:has(img)) {
    padding: 1rem;
}

浏览器得出的结果正是我们所期望的:

img

Demo 地址: https://codepen.io/airen/full/jOeBvzY

.card:not(:has(img)) 的意思是“选择一个不包含 img 的 .card”,这正是我们在这种情况下想要的。

也就是说,:has(:not(img)) 与 :not(:has(img)) 的差异在于它们选择元素的方式。

  • :has(:not(img)) 的意思是“选择包含任何非 img 元素的元素”。因此,它会选择包含任何非 img 元素的父元素(在这种情况下,是 .card 元素)。
  • :not(:has(img)) 的意思是“选择不包含 img 元素的任何元素”。因此,它会选择不包含 img 元素的 .card 元素。

因此,两个选择器选择的元素是不同的。:has(:not(img)) 选择的是包含非 img 元素的 .card 元素,而 :not(:has(img)) 选择的是不包含 img 元素的 .card 元素。

这有点绕口或更易于引起对它们的混淆。但就此示例而言,我们可以使用更简单的选择器,比如选中不带图片(img)的 .card ,我们可以使用 .card:not(:has(img)) ;反之,如果选择带有 img 的 .card 则可以使用 .card:has(img) :

.card:has(img) {
    outline: 3px solid red;
}

.card:not(:has(img)) {
    outline: 3px solid blue;
}

img

Demo 地址:https://codepen.io/airen/full/ExdWedJ

:has() 和 :not() 构建高级选择器

:has() 和 :not() 选择器组合使用,除了能帮助你选中所需要的元素之外,还可以构建一些高级选择器。比如,选择多个范围的组、模拟 :nth-child(An+B [of S]?) 和选择一个具有特定类的兄弟元素的组中的最后一个元素等。

选择多个范围的组

在 CSS 中,我们可以使用 :has() 选择器和通用兄弟组合选择器(~)选择一个范围组的元素。例如,我们有下面这样的一个 HTML 结构:

<ul>
    <li class="rect"></li>
    <li class="rect"></li>
    <li data-range></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li data-range></li>
    <li class="star"></li>
    <li class="star"></li>
    <li class="star"></li>
    <li class="rect"></li>
</ul>

为了演示,我们在其中的两个列表项中定义了一个名为 data-range 的属性,主要用它们来确定范围的开始和结束。我们可以使用 :has() 和 ~ 选择器组合在一起,选中 data-range 开始和结束之间所有 li (即所有 .circle 元素):

[data-range] ~ :has(~ [data-range]) {
    width: 100px;
    border: 2px solid #09f;
    outline: 4px solid rgb(0 0 0 / 0.5);
}

img

Demo 地址:https://codepen.io/airen/full/VwEpEMb

而且我们可以分别使用:

  • [data-range]:has(~ [data-range]) 给范围起始元素(第一个设置 data-range 的 li 元素)设置样式
  • [data-range] ~ [data-range] 给范围结束元素(第二个设置 data-range 的 li 元素)设置样式
/* 选择一个范围的起始元素 */
[data-range]:has(~ [data-range]) {
    background-color: #987;
    outline: 2px solid red;
}

/* 选择一个范围的结束元素 */
[data-range] ~ [data-range] {
    background-color: #90f;
    outline: 2px solid orange;
}

img

Demo 地址:https://codepen.io/airen/full/dygvQbJ

现在,我们将推进我们之前的演示,并解决选择多个范围的问题。像上面示例一样,在列项上使用数据属性 data-range 在单个父元素内创建多个范围组设置可区分的起始和结束标记,并且设置起始标记的值为 start (即 data-range="start")和结束标记的值为 end (即 data-range="end"):

<ul>
    <li data-range="start"></li>
    <li class="rect"></li>
    <li class="rect"></li>
    <li data-range="end"></li>
    <li data-range="start"></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li class="circle"></li>
    <li data-range="end"></li>
    <li data-range="start"></li>
    <li class="star"></li>
    <li class="star"></li>
    <li class="star"></li>
    <li data-range="end"></li>
    <li class="rect"></li>
</ul>

由于我们给自定义属性设置了具体的值,因此使用 CSS 属性选择器就可以很轻易地选中一个组中的起始元素和结束元素:

/* 选择范围组中开始和结束元素 */
[data-range] {
    box-shadow: 0 0 0 6px red;
    border-radius: 2px;
}

/* 选择范围组中起始元素 */
[data-range="start"] {
    outline: 3px solid yellow;
}

/* 选择范围组中结束元素 */
[data-range="end"] {
    outline: 3px solid #e90;
}

img

让我们把范围组中的第一个和最后一个元素选出来。这里使用了排除条件 :not([data-range]) 选择器,将自定义属性 data-range 没有 start 和 end 的标记排除出去:

/* 选择范围组内的第一个元素 */
[data-range="start"] + :has(~ [data-range="end"]):not([data-range]) {
    width: 80px;
    border: 4px solid #09f;
}

/* 选择范围组内的最后一个元素 */
[data-range="start"] ~ :has(+ [data-range="end"]):not([data-range]) {
    width: 80px;
    border: 4px solid;
}

img

最后,我们需要选择器来匹配范围内的列表项。一开始,它与我们之前为“范围内”选择器创建的相似。同样,我们添加了一个条件,即它不匹配一个本身是 [data-range] 元素的元素。

/* 选择范围组内的所有元素 */
[data-range="start"] ~ :has(~ [data-range="end"]):not([data-range]) {
    width: 80px;
    border: 4px solid;
    box-shadow: 0 0 0 4px rgb(0 0 0 / 0.125);
}

img

需要注意的是,通用兄弟组合选择器(~)是具有跳跃能力的。因此,上面的选择器选中的范围将会超出我们的预期范围。为了解决这个问题,我们需要添加一个更为复杂的条件选择器,即 AND 条件,使用 :not() 选择器来排除那些不在 [data-range="end"] 和 [data-range="start"] 之间的列表项。

但是,如果你还记得,我提到过通用兄弟选择器具有跳跃能力,所以目前,该选择器将会样式化超出我们预期范围的项目。下面的图片展示了在没有进一步限制如何应用规则的情况下,该规则如何工作。也就是说,单独使用这部分的选择器,将会告诉浏览器“不要选择跟随 [data-range="end"] 的列表项,这些列表项还有一个后续名为 [data-range="start"] 的兄弟元素”。

[data-range="start"]
    ~ :has(~ [data-range="end"]):not([data-range]):not(
        [data-range="end"] ~ :has(~ [data-range="start"])
    ) {
    background-image: linear-gradient(
        to bottom left in oklab,
        oklch(55% 0.45 350) 0%,
        oklch(100% 0.4 95) 100%
    );
}

img

Demo 地址:https://codepen.io/airen/full/BaqWGmX

总体来说,这是一个相当冗长但非常强大的选择器,之前在没有 CSS 中的“向前查看”和“向后查看”能力的情况下,要想实现这个效果就必须使用 JavaScript。

上面示例展示了使用 :has() 和 :not() 选择器实现多范围组的选择,但还是需要依赖于手动在 HTML 中添加“钩子”(data-range="start" 和 data-range="end")。如果我们换成有一个包含复选框的字段集,则可以再次使用 :checked 状态选择器来自动地、清晰地标识已选和未选项之间的边界。它们的结合可以允许你基于状态的动态样式变化,创建视觉边界变得更加容易。比如下面这个示例,当复选框被选中时,所选中的复选框将会变成一个范围组,会有一个边框和背景的视觉效果。

img

Demo 地址:https://codepen.io/airen/full/XWxRjOo

实现上图的效果,你需要一个 HTML 结构:

<div class="card">
    <header>
        <h3>请选择你喜欢的语言</h3>
    </header>
    <div class="card__content">
        <label for="css"
            ><input type="checkbox" name="css" id="css" />CSS</label
        >
        <label for="html"
            ><input type="checkbox" name="html" id="html" checked />HTML</label
        >
        <label for="js"
            ><input type="checkbox" name="js" id="js" />JavaScript</label
        >
        <label for="vue"
            ><input type="checkbox" name="vue" id="vue" checked />Vue</label
        >
        <label for="react"
            ><input type="checkbox" name="react" id="react" />React</label
        >
        <label for="php"
            ><input type="checkbox" name="php" id="php" />PHP</label
        >
        <label for="java"
            ><input type="checkbox" name="java" id="java" />Java</label
        >
        <label for="python"
            ><input type="checkbox" name="python" id="python" />Python</label
        >
    </div>
</div>

关键 CSS 代码:

/* 自定义复选框 UI 样式 */
input[type="checkbox"] {
    height: 1px;
    overflow: hidden;
    width: 1px;
    position: absolute;
    clip-path: inset(50%);
}

label {
    display: flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.5em;
    border: 1px solid transparent;
    border-radius: var(--radius-tl, 0) var(--radius-tr, 0) var(--radius-br, 0)
        var(--radius-bl, 0);
}

label::before {
    content: "";
    display: inline-flex;
    justify-content: center;
    align-items: center;
    border-radius: 4px;
    border: 1px solid #c5c5c5;
    background: #fff;
    color: #c5c5c5;
    width: 24px;
    aspect-ratio: 1;
    transition: all 0.2s ease;
}

label:has(:checked) {
    color: #9739e8;
}

label:has(:checked)::before {
    content: "✔";
    border-color: #9739e8;
    background-color: #9739e8;
    color: #fff;
    font-size: 0.75em;
}

/* 有状态的多范围选择组 */

/* 范围组内的第一个选中项或单个选中项的顶部样式 */
label:has(:checked):not(label:has(:checked) + label) {
    --radius-tl: 0.75em;
    --radius-tr: 0.75em;
    border-block-start-color: black;
}

/* 范围组内最后一次选中的项或单个选中项的底部样式 */
label:has(:checked):not(label:has(+ label :checked)) {
    --radius-bl: 0.75em;
    --radius-br: 0.75em;
    border-block-end-color: black;
    box-shadow: 0 4px 3px -2px rgba(0, 0, 0, 0.35);
}

/* 范围组内所有项的样式 */
label:has(:checked):has(~ label :checked),
label:has(:checked):not(label:has(+ label :checked)) {
    border-inline-color: black;
    background-color: palegreen;
}

简单解释一下。我们需要对范围组或单个选中项目的顶部、中间和底部定义 UI 样式。

通过 label:has(:checked):not(label:has(:checked) + label) 选择器来选中范围组的第一个项目或单个项目,然后给其顶部设置样式,即添加圆角和顶部边框:

/* 范围组内的第一个选中项或单个选中项的顶部样式 */
label:has(:checked):not(label:has(:checked) + label) {
    --radius-tl: 0.75em;
    --radius-tr: 0.75em;
    border-block-start-color: black;
}

同样的,通过 label:has(:checked):not(label:has(+ label :checked)) 选择器来选中范围组的最后一项或单个项目,然后给其底部设置样式,也是添加圆角和底部边框:

/* 范围组内最后一次选中的项或单个选中项的底部样式 */
label:has(:checked):not(label:has(+ label :checked)) {
    --radius-bl: 0.75em;
    --radius-br: 0.75em;
    border-block-end-color: black;
    box-shadow: 0 4px 3px -2px rgba(0, 0, 0, 0.35);
}

注意,其实单个选中项目也是一范围组,是最小范围组,它既是范围组中的第一个项目,也是范围组中的最后一个项目。

最后就是选中项目设置样式。其实只使用 label:has(:checked) 就可以选中状态为 :checked 的复选框(已选复选框),但我们这里是一个多范围组的演示,所以会复杂一些:

/* 范围组内所有项的样式 */
label:has(:checked):has(~ label :checked),
label:has(:checked):not(label:has(+ label :checked)) {
    border-inline-color: black;
    background-color: palegreen;
}

模拟 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器

:nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器也是结构伪类选择器中的一部分。正如 @Stefan Judis 在 Twitter 上所说,浏览器对其支持度还是有限的:

img

目前可以在 Chromium 111、Safari 9 和 Firefox 113 看到该选择器的效果:

img

Demo 地址:https://codepen.io/airen/full/YzJVpLQ

不过,我在这里不想介绍 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器如何使用。我想和大家一起来探讨,如何使用 :has() 和 :not() 模拟出它们一样的功能。

在开始之前,我们明确一个概念“元素组”,即可以被分组在一起的相邻兄弟元素的一组。例如下面这个列表:

<ul>
    <li>No Class Name</li>
    <li></li>

    <li class="special">Special</li>
    <li class="special">Special</li>
    <li class="special">Special</li>

    <li>No Class Name</li>
    <li></li>

    <li class="special">Special</li>
    <li class="special">Special</li>

    <li>No Class Name</li>
    <li></li>
    <li>No Class Name</li>
    <li></li>

    <li class="special">Special</li>

    <li>No Class Name</li>
    <li></li>
</ul>

img

正如上图所示,列表中的第 2、3 和 4 个 li 构成一个元素组,因为它们共享同一个类名 .special ,所以被分组在一起。同样地,列表中的第 6 和 7 个 li 也是如此,它们也组成了一个元素组。甚至列表中的第 10 个 li 也是一个元素组,即使它只包含单个 li 元素。

接下来,将使用 :has() 和 :not() 选择器来选中我们希望选中的元素。

首先来看选中“元素组”中的第一个元素。

.😍:not(.😍 + .😍) {
    background: #09f;
}

这个选择器将会选中“元素组”中的第一个元素:

img

Demo 地址:https://codepen.io/airen/full/PoymmPj

.😍 选择所有具有 .😍 的元素。通过追加 :not(.😍 + .😍),我们将排除前面有 .😍 的 .😍。

再来看第二个选择器,我们可以通过下面这个选择器来选中“元素组”中最后一个元素:

.😍:not(:has(+ .😍)) {
    background: #f35;
}

它的工作原理是选择任何不直接跟在另一个 .😍 后面的 .😍。

img

Demo 地址:https://codepen.io/airen/full/jOemmVv

注意,在此示例中,.😍:not(.😍 + .😍) 选择器权重是 (0, 3, 0) ,而 .😍:not(:has(+ .😍)) 选择器权重是 (0, 2, 0) ,因此,你看到第 11 个列表项 li 的背景依旧是 #09f 。

你可以通过 .😍:not(.😍 + .😍) (选中“元素组”中第一个元素)和 .😍:not(:has(+ .😍)) (选中“元素组”中最后一个元素)相结合,来选中仅一个元素组成的“元素组”:

.😍:not(.😍 + .😍):not(:has(+ .😍)) {
    background-color: #890aef;
}

这个选择器选择那些没有前置 .😍 和没有后置 .😍 兄弟的 .😍 元素。

img

Demo 地址:https://codepen.io/airen/full/GRYmmOM

更为有意思的是,通过添加更多的 .😍 条件,可以让你不仅限于选择“元素组”中的第一个或最后一个元素,你可以像 :nth-child(n) 选择器一样,选择“元素组”中的第 n 个元素:

/* 选择元素组中第二个元素 */
.😍:not(.😍 + .😍) + .😍 {
    background-color: #09f;
}

/* 选择元素组中第三个元素*/
.😍:not(.😍 + .😍) + .😍 + .😍 {
    background-color: #f35;
}

img

Demo 地址: https://codepen.io/airen/full/QWZvvVz

同样的,要条件中添加更多的 + .😍 选择器,可以实现类似 :last-nth-child() 选择器,从“组元素”中后面向前数的第 n 个元素:

/* 选择元素组中倒数第二个元素 */
.😍:not(:has(+ .😍 + .😍)):has(+ .😍) {
    background-color: #09f;
}

/* 选择元素组中倒数第三个元素*/
.😍:not(:has(+ .😍 + .😍 + .😍)):has(+ .😍 + .😍) {
    background-color: #f35;
}

img

Demo 地址: https://codepen.io/airen/full/WNajOrB

正如你所看到的,我们可以使用 :has() 、:not() 、~ 和 + 组合在一起,实现:

  • :first-in-ElementGroups-of-class(.😍) ,即选中元素组(ElementGroups)中第一个元素(类名为 .😍),对应的选择器为 .😍:not(.😍 + .😍)) 。
  • :last-in-ElementGroups-of-class(.😍) ,即选中元素组(ElementGroups)中最后一个元素(类名为 .😍),对应的选择器为 .😍:not(:has(+ .😍)) 。
  • :single-in-ElementGroups-of-class(.😍) ,即选中元素组(ElementGroups)中仅有的一个元素(类名为 .😍),对应的选择器为 .😍:not(.😍 + .😍):not(:has(+ .😍)) 。
  • :nth-in-ElementGroups-of-class(.😍) ,即选中元素组(ElementGroups)中的第 n 个元素(类名为 .😍)。比如 .😍:not(.😍 + .😍) + .😍 选择元素组中第 2 个元素(类名为 .😍);.😍:not(.😍 + .😍) + .😍 + .😍 选择元素组中第 3 个元素(类名为 .😍)。
  • :nth-last-in-island-of-class(.special),即选中元素组(ElementGroups)中的倒数第 n 个元素(类名为 .😍)。比如 .😍:not(:has(+ .😍 + .😍)):has(+ .😍) 选中元素组中倒数第 2 个元素(类名为 .😍),.😍:not(:has(+ .😍 + .😍 + .😍)):has(+ .😍 + .😍) 选中元素组中倒数第 3 个元素(类名为 .😍)。

你可以写一个简单的小工具,自动生成相应的选择器。

img

Demo 地址:https://codepen.io/airen/full/jOemweW

基于该原理,你也可以使用 :has() 、:not() 和 ~ 组合在一起来模拟 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?)。

先从简单的着手,使用下面选择器可以模拟出 :nth-child(2 of .🦵) 选择器,即 :nth-child(B of S):

.🦵 ~ .🦵:not(.🦵 ~ .🦵 ~ .🦵) {
    background-color: #09f;
}

img

Demo 地址:https://codepen.io/airen/full/QWZvMMv

它的工作原理是:

  • 使用 .🦵 ~ .🦵 选择所有前面有 1 个 .🦵 的 .🦵 ,从而有效地选择第 2 个、第 3 个、第 4 个、第 n 个 .🦵 兄弟元素。简单地说,会选择除了第一个 .🦵 之外的所有 .🦵
  • 将该选择限制为排除任何前面有 2 个 .🦵 的 .🦵,从而从初始选择中排除第 3 个、第 4个、第 n 个 .🦵 兄弟元素。

也就是说,你可以通过在选择器中添加更多的 ~ .🦵 部分,来选择第 3 个、第 4 个、第 5 个、第 n 个 .🦵 兄弟元素,从而实现 :nth-child(B of S) 选择器。

模拟 :nth-last-child(B of S) 选择器是相似的。比如,使用下面的代码可以模拟出 :nth-last-child(3 of .🦵) 选择器:

.🦵:not(:has(~ .🦵 ~ .🦵 ~ .🦵)):not(.🦵:not(:has(~ .🦵 ~ .🦵))) {
    background-color: #09f;
}

img

Demo 地址: https://codepen.io/airen/full/QWZvMeQ

它的工作原理是:

  • 使用 .🦵:not(:has(~ .🦵 ~ .🦵 ~ .🦵)) 选择最后 3 个 .🦵
  • 使用 .🦵:not(.🦵:not(:has(~ .🦵 ~ .🦵))) 选择除最后 2 个 .🦵 之外的所有 .🦵

从两个选择中取交集就得到了 :nth-last-child(3 of .🦵)。

同样地,你可以构建一个简单的工具,自动生成所需要的选择器:

img

Demo 地址:https://codepen.io/airen/full/rNqmGrN

这样做主要是因为 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器需要在 Chromium 111+、Safari 9+ 和 Firefox 113+ 上才能得到支持。而 :has() 和 :not() 选择器得到浏览器支持的版本要更早。

img

如果你想在和 :has() 以及 :not() 选择器同浏览器版本上使用 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器,前面的内容尤其有意义。反之,仅是使用 :has() 和 :not() 来模拟 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 选择器的话,意义不大。

事实上,这里仅是想通过这些实例来告诉大家 :has() 和 :not() 选择器以及 ~ 和 + 选择器的组合,可以构建出强大的 CSS 选择器。

小结

在这节课中,主要向大家介绍了 :has() 和 :not() 选择器的差异,以及它们的相互组合能构建出强大的 CSS 选择器。正如课程中示例所展示,:has() 、:not() 、~ 和 + 组合在一起,可以让你选择单个或多个范围内的元素、也可以模拟出 :nth-child(An+B [of S]?) 和 :nth-last-child(An+B [of S]?) 等选择器的功能。虽然这些选择器冗余和复杂,但其功能是强大的。

另外,这些组合选择器也从侧面告诉大家 :has(:not()) 与 :not(:has()) 是完全不同的。我们在使用它们的组合时,尤其需要注意这一点。

最后想说的是,虽然这些复杂的选择器我们用到的时候很少,但是希望大家在使用 :has() 和 :not() 选择器时,能打破固有的思维,创造出更适合生产与使用的高级选择器。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
02.CSS选择器:has()能解决什么问题
Next
04.CSS选择器::where()vs.:is()