:has():系列选择器

自创世以来(用 CSS 术语来说),我们处理了各种意义上的级联。我们的样式构成了“层叠样式表”。我们的选择器也会进行级联。它们可以侧向移动。在大多数情况下,它们会呈现出下降趋势。但绝不能向上。多年来,我们一直构想一个“父级选择器”。现在终于来了!采用 :has() 伪选择器的形状。

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

但它不只是一个“父级”选择器。这种宣传方式很不错。不太有吸引力的方式可能是“条件环境”选择器。但这根指的圆环就不一样了。改用“家庭”选择器怎么样?

浏览器支持

在继续进行深入讨论之前,有必要介绍一下浏览器支持。目前还不行。不过,离目标更近了。目前还没有 Firefox 支持,我们已将其纳入路线图。不过,Safari 中已提供该版本,并且将于 Chromium 105 中推出。本文中的所有演示都会告诉您所使用的浏览器是否不支持这些演示。

如何使用 :has

但它到底是什么样子?请考虑以下 HTML,该 HTML 具有两个类为 everybody 的同级元素。您将如何选择其后代具有 a-good-time 类的 activity?

<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) { ... } Select every solo a包含某个条件a包含某类元素aarticlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … }选择一个标题后跟副标题的 article css article:has(> h1 + h2) { … } 在触发互动状态时选择 :root css :root:has(a:hover) { … } 选择 figure 后面的段落且没有 figcaption 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 调整。减少了 HTML,因为您不再需要 card card--has-media 等类。

跳出思维定式

如上所述,: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;
}

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

想不想试试这个经典的《Buzz 线》游戏呢?使用 :has() 可以更轻松地创建机制。如果电线悬停在导线上,游戏结束。可以,我们可以使用同级的组合器+~)来创建其中一些游戏机制。但是,: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() 相关的实际性能指标,请查看此故障。感谢 Byungwoo,感谢他与大家分享这些与实施相关的见解和细节。

大功告成!

:has()做好准备。不妨把这件事告诉朋友并分享这篇博文,它将彻底改变我们实施 CSS 的方式。

CodePen 集合中提供了所有演示。