:has():系列选择器

自从 CSS 诞生以来,我们就一直在使用各种意义上的级联。我们的样式构成了“层叠样式表”。我们的选择器也会级联。它们可以横向显示。在大多数情况下,它们会向下。但绝不会向上。多年来,我们一直在构想“父级选择器”。现在,它终于要来了!采用 :has() 伪选择器的形式。

如果作为参数传递的任何选择器与至少一个元素匹配,则 :has() CSS 伪类表示该元素。

但它不仅仅是一个“父级”选择器。这是一个不错的营销方式。不太理想的方法可能是“基于条件的环境”选择器。但这听起来不太一样。“family”选择器如何?

浏览器支持

在继续之前,有必要提一下浏览器支持。还没有达到这个水平。不过,我们离实现这一目标越来越近了。尚不支持 Firefox,但已将这项支持纳入路线图。但它已在 Safari 中推出,并将在 Chromium 105 中发布。如果所用浏览器不支持本文中的所有演示,系统会告知您。

如何使用 :has

但它到底是什么样子?请考虑以下 HTML,其中包含两个类为 everybody 的兄弟元素。如何选择具有类 a-good-time 的后代?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

借助 :has(),您可以使用以下 CSS 实现此目的。

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

这会选择 .everybody 的第一个实例并应用 animation

在此示例中,类为 everybody 的元素是目标。条件是具有类 a-good-time 的后代。

<target>:has(<condition>) { <styles> }

不过,您可以做的远不止这些,因为 :has() 提供了许多机会。甚至包括您可能尚未发现的娱乐内容。请考虑以下几点。

选择具有直接 figcaptionfigure 元素。css figure:has(> figcaption) { ... } 选择没有直接 SVG 后代的 anchor css a:not(:has(> svg)) { ... } 选择具有直接 input 同级兄弟的 label。横向移动! css label:has(+ input) { … } 选择子代 img 不含 alt 文本的 article css article:has(img:not([alt])) { … } 选择 DOM 中存在某种状态的 documentElement css :root:has(.menu-toggle[aria-pressed=”true”]) { … } 选择子代数为奇数的布局容器 css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } 选择网格中未被悬停的所有项 css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } 选择包含自定义元素 <todo-list> 的容器 css main:has(todo-list) { ... } 选择段落中具有直接同级兄弟 hr 元素的每个单独 a css p:has(+ hr) a:only-child { … } 选择满足多个条件的 article css article:has(>h1):has(>h2) { … } 混合使用。选择标题后跟副标题的 article css article:has(> h1 + h2) { … } 选择触发互动状态时的 :root css :root:has(a:hover) { … } 选择不带 figcaptionfigure 后面的段落 css figure:not(:has(figcaption)) + p { … }

您能想到 :has() 的哪些有趣用例?有趣的是,它会鼓励您打破自己的思维模型。这会让您思考“我能否以其他方式处理这些样式?”。

示例

我们来看一些示例,了解如何使用它。

卡片

制作一个传统卡片演示。我们可以在卡片中显示任何信息,例如标题、字幕或某些媒体。以下是基本卡片。

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

如果您想添加一些媒体内容,会怎么样?对于这种设计,卡片可以拆分为两列。以前,您可能需要创建一个新类来表示此行为,例如 card--with-mediacard--two-columns。这些类名称不仅难以想起,而且难以维护和记住。

借助 :has(),您可以检测卡片是否包含媒体内容,并执行相应操作。无需修饰符类名称。

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

您无需将其留在原处。您可以发挥创意。显示“精选”内容的卡片在布局中可能会如何自适应?此 CSS 会使精选卡片占据布局的整个宽度,并将其放置在网格的开头。

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

如果带有横幅的精选卡片为了吸引注意力而抖动,会怎么样?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

无限可能。

表单

表单呢?它们以难以设置样式而闻名。例如,您可以设置输入及其标签的样式。例如,如何指明字段有效?借助 :has(),这变得更加容易。我们可以钩入相关的表单伪类,例如 :valid:invalid

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

请在此示例中试用:尝试输入有效和无效的值,并将焦点切换到和切换出。

您还可以使用 :has() 显示和隐藏字段的错误消息。选择“电子邮件”字段组,然后为其添加错误消息。

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

默认情况下,您会隐藏错误消息。

.form-group__error {
  display: none;
}

但是,当该字段变为 :invalid 且未获得焦点时,您无需额外的类名称即可显示消息。

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

您完全可以为用户与表单互动时添加一些恰到好处的奇思妙想。请参考下面的示例。输入微互动有效值时,请留意。:invalid 值会导致表单组抖动。但前提是用户没有任何动作偏好设置。

内容

我们在代码示例中提到过这一点。但是,如何在文档流程中使用 :has()?例如,它可以为我们提供有关如何为媒体设置排版样式的想法。

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

此示例包含图表。如果没有 figcaption,则会在内容中漂浮。存在 figcaption 时,它们会占据全宽并获得额外的边距。

响应状态

不妨让您的样式对标记中的某些状态做出响应。我们来看一个使用“传统”滑动导航栏的示例。如果您有一个用于切换导航栏打开状态的按钮,则该按钮可能会使用 aria-expanded 属性。您可以使用 JavaScript 更新相应的属性。当 aria-expandedtrue 时,使用 :has() 检测这一点并更新滑动导航栏的样式。JavaScript 会完成自己的工作,CSS 可以根据需要使用这些信息。无需重新排列标记或添加额外的类名称等(注意:这不是可用于生产环境的示例)。

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

:has 是否有助于避免用户出错?

所有这些示例有什么共同之处?除了展示 :has() 的使用方法之外,这些示例都不需要修改类名称。他们各自插入了新内容并更新了属性。这是 :has() 的一个重要优势,因为它有助于减少用户错误。借助 :has(),CSS 能够承担根据 DOM 中的修改进行调整的责任。您无需在 JavaScript 中处理类名称,从而降低了开发者出错的可能性。我们都曾输错过类名称,并不得不将其保留在 Object 查找中。

这是一个有趣的想法,它能否帮助我们编写更简洁的标记和更少的代码?由于我们减少了 JavaScript 调整,因此 JavaScript 会更少。由于不再需要 card card--has-media 等类,因此 HTML 代码更少。

跳出思维定式

如上所述,:has() 鼓励您打破思维模型。这是一个尝试不同事物的机会。尝试突破界限的一种方法是,仅使用 CSS 来制作游戏机制。例如,您可以使用表单和 CSS 创建基于步骤的机制。

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

这为我们开启了有趣的可能性。您可以使用它通过转换遍历表单。请注意,最好在单独的浏览器标签页中查看此演示。

为了增加趣味,不妨来玩一玩经典的触电游戏吧!借助 :has(),您可以更轻松地创建此机制。如果鼠标悬停在电线上,游戏就会结束。是的,我们可以使用同级 combinator+~)等内容来创建其中的一些游戏机制。不过,:has() 可以实现相同的结果,而无需使用有趣的标记“技巧”。请注意,最好在单独的浏览器标签页中查看此演示。

虽然您不会很快将这些代码放入生产环境,但它们突出显示了使用基元的方法。例如,能够链接 :has()

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

性能和限制

在结束之前,请问您无法使用 :has() 执行哪些操作?:has() 有一些限制。主要原因是性能受到影响。

  • 您无法:has() :has()。但您可以串联 :has()css :has(.a:has(.b)) { … }
  • :has() css :has(::after) { … } :has(::first-letter) { … } 中未使用伪元素
  • 限制在仅接受复合选择器的伪元素中使用 :has() css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • 限制在伪元素 css ::part(foo):has(:focus) { … } 后使用 :has()
  • 使用 :visited 将始终为 false css :has(:visited) { … }

如需了解与 :has() 相关的实际效果指标,请参阅此 Glitch。感谢 Byungwoo 分享这些有关实现的深入分析和详细信息。

大功告成!

做好迎接 :has() 的准备。请告诉您的好友并分享这篇文章,它将彻底改变我们对 CSS 的处理方式。

所有演示均可在此 CodePen 集合中找到。