:has()

Avatar of Mojtaba Seyedi
Mojtaba Seyedi

DigitalOcean 提供适用于您旅程各个阶段的云产品。立即开始使用 200 美元的免费积分!

CSS :has() 伪类选择包含其他元素的元素,这些元素匹配传递给其参数的选择器。由于它能够根据包含的子元素选择父元素并向父元素应用样式,因此它通常被称为“父选择器”。

/* Select the .card element when it 
   contains a <figure> immediately
   followed by a paragraph. */
.card:has(figure + p) {
  flex-direction: row;
}

此示例选择具有 .card 类的元素,当它包含一个紧跟在 <p> 元素之后的 <figure> 元素时。

<article class="card">
  <figure></figure>
  <p></p>
</article>

这对于为可能或可能不包含某些元素的组件编写样式非常有用,例如卡片网格,其中卡片项始终包含段落,但可能没有附带的图像。

有图像还是没有图像?这是一个问题。

这样,在您不知道标记包含哪些内容的情况下,您仍然可以编写与这些条件匹配的样式。

:has()选择器级别 4 规范 中定义,它被描述为“关系伪类”,因为它能够根据元素与其他元素的关系匹配选择器。

基本用法

以下 HTML 包含两个 <button> 元素。其中一个包含 SVG 图标。

<!-- Plain button -->
<button>Add</button>

<!-- Button with SVG icon -->
<button>
  <svg></svg>
  Add
</button>

现在,假设您想仅将样式应用于 <button> 元素,该元素内部包含 <svg> 元素。

:has() 非常适合这项工作

button:has(svg) {
  /* Styles */
}

:has() 选择器使我们能够区分一个包含后代 <svg> 的按钮和一个不包含后代 <svg> 的按钮。

语法

:has( <unforgiving-relative-selector-list> )

不可饶恕的选择器列表指的是传递给 :has() 选择器参数的参数,它是一个逗号分隔的元素列表,这些元素根据它们与父元素的关系一起进行评估。

article:has(ol, ul) {
  /* Matches an <article> that contains either
     an ordered or unordered list. */
}

我们将稍后更详细地介绍参数列表的“不可饶恕”性质。

特异性

:has() 的一个更有趣的方面是,它的特异性由其参数列表中最具体的元素决定。假设我们有以下样式规则

article:has(.some-class, #id, img) {
  background: #000;
}

article .some-class {
  background: #fff;
}

我们有两个规则,都选择一个 <article> 元素来更改其背景。此 HTML 获取哪个背景?

<article class="some-class">
  <div class="some-class"></div>
</article>

您可能会认为它获得了白色 (#fff) 背景,因为它在 级联 中排在后面。但是,由于 :has() 的参数列表包含其他选择器,因此我们必须考虑列表中最具体的选择器才能确定第一个规则的实际特异性。在本例中,最具体的将是 #id

让我们比较一下

  • article .some-class 生成得分 (0,1,1)
  • article:has(.some-class, #id, img) 生成得分 (1,0,1)

第一个规则获胜!该元素获得黑色 (#000) 背景。

小测验!

您认为以下示例中哪个颜色获胜?

article:has(h1, .title) a { 
  color: red; 
}

article h1 a {
  color: green;
}
告诉我答案!
/* Specificity: (0,1,2) */
article:has(h1, .title) a {
  color: red; /* 🏆 Winner! */
}

/* Specificity: (0,0,3) */
article h1 a {
  color: green;
}

:has() 是一个“不可饶恕”的选择器

规范的第一个草案将 :has() 引入为一个 “宽容的选择器”

:has( <forgiving-relative-selector-list> )

这个想法是,一个列表可以包含一个无效选择器并忽略它。

/* Example: Do not use! */
article:has(h2, ul, ::-scoobydoo) { }

::-scoobydoo 是一个完全虚构的、不存在的无效伪元素。如果 :has() 是一个“宽容”选择器,那么该错误选择器将被简单地忽略,而其他参数将按正常方式解析。

但后来,由于与 jQuery 行为的 冲突,规范作者决定将 :has() 定义为一个非宽容选择器。因此,:is() 和 :where() 是其中唯一宽容的相关选择器。

这意味着 :has() 的行为更像一个 复合选择器。根据 CSS 规范,出于遗留原因,复合选择器的通用行为是,如果列表中的任何选择器无效,则整个选择器列表均无效,导致整个规则集被丢弃。

/* This doesn't do anything because `::-scoobydoo`
   is an invalid selector */
a, a::-scoobydoo {
  color: green;
}

:has() 也是如此。其参数列表中的任何无效选择器都将使列表中的所有其他内容失效。因此,我们之前看到的那个示例

/* Example: Do not use! */
article:has(h2, ul, ::-scoobydoo) { }

…将完全不会执行任何操作。参数列表中的所有三个选择器都因无效的 ::scoobydoo 选择器而失效。这是一个全有或全无的事情。

但有一种解决方法。请记住,:is():where() 是宽容的,即使 :has() 不是。这意味着我们可以将其中任何一个选择器嵌套在 :has() 中以获得更宽容的行为

p:has(:where(a, a::scoobydoo)) {
  color: green;
}

因此,如果您希望 :has() 作为“宽容”选择器工作,请尝试将 :is() 或 :where() 嵌套在其中。

参数列表接受复杂选择器

一个 复杂选择器 包含一个或多个复合选择器(例如 a.fancy-link)和组合器(例如 >、 +、 ~)。:has() 的参数列表接受这些复杂选择器,可用于识别多个元素之间的关系。

<relative-selector> = <combinator>? <complex-selector>

这是一个包含使用 子组合器 (>) 的复杂选择器的相关选择器的示例。它选择具有 .icon 类的元素,这些元素是具有 .fancy-link 类的链接的直接子元素,并且处于 :focus 状态

a.fancy-link:focus > .icon {
  /* Styles */
}

这种方法可以直接在 :has() 参数列表中使用

p:has(a.fancy-link:focus > .icon) {
  /* Styles */
}

但我们并不是选择作为处于 :focus 状态的 .fancy-class 链接的直接子元素的 .icon 元素,而是对包含具有 .icon 类的直接子元素的已聚焦 .fancy-links 的段落进行样式设置。

哎呀,试着快速说三遍!也许看到一个匹配的标记示例会有所帮助

通常不支持伪选择器

我说 :has()“通常”不支持其参数中的其他伪元素,因为这正是规范中的说法

注意: 伪元素通常被 :has() 排除在外,因为它们中的许多是根据其祖先的样式有条件地存在的,因此允许 :has() 查询这些元素会导致循环。

实际上,在 `:has()` 的参数列表中,确实允许使用一些“允许的伪元素”。规范提供了以下示例,展示了如何将 `:not()` 与 `:has()` 结合使用。

/* Matches any <section> element that contains 
   anything that’s not a heading element. */
section:has(:not(h1, h2, h3, h4, h5, h6))

Jhey Tompkins 提供了另一个示例,展示了如何使用 `:has()` 根据各种输入状态(例如 `:valid`、`invalid` 和 `:placeholder-shown`)来设置表单的样式。

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()` 嵌套在 `:has()` 中。

/* Nesting is a no-go */
.header-group:has(.subtitle:has(h2)) {
  /* Invalid! */
}

这将创建一个无限循环,在其中特异性将在另一个评估中进行评估。但是,它确实允许您链接参数。

h2,
.subtitle {
  margin-block-end: 1.5rem;
}

/* Reduce spacing on header because the subtitle will handle it */
.header-group:has(h2):has(.subtitle) h2 {
  margin-block-end: 0.2rem;
}

链式调用类似于 `AND` 逻辑运算符,即两个条件都必须匹配才能使样式规则生效。假设您有一系列 `.news-articles`,其中包含的各篇文章都属于不同的类别。也许您想对该列表应用某些样式,但仅当它包含带有 `.breaking-news` 和 `.featured-news` 类别的文章时才应用,而如果只匹配其中一个或没有匹配任何一个,则保持不变。

那么,您可以为这种条件样式链式调用两个 `:has()` 声明。

.news-list:has(.featured-news):has(.breaking-news) { 
  /* Styles */ 
}

此示例仅针对 `.news-list` 容器。如果我们想匹配任何具有 `.featured-news` 和 `.breaking-news` 类的文章的父元素,我们可以完全省略 `.news-list`。

:has(.featured-news):has(.breaking-news) { 
  /* Styles */ 
}

它不仅仅是一个“父”选择器。

Jhey Thompkins 称之为“家族”选择器,这可能是一个更贴切的描述,特别是在我们最后看过的那个示例中。让我们再看一次。

.header-group:has(h2):has(.subtitle) h2 {
  margin-block-end: 0.2rem;
}

我们不仅仅选择包含 `<h2>` 元素的带有 `.header-group` 类的元素。这就是通常归因于 `:has()` 的父选择能力。我们选择的是一个元素。

  • 带有 `.subtitle` 类。
  • 是 `.header-group` 的子元素。
  • 并在其内部包含 `<h2>` 元素。

`<h2>` 是 `.header-group` 的直接子元素吗?不,更像是孙子元素。

将 `:has()` 与其他关系伪选择器结合使用。

您可以将 `:has()` 与其他函数式伪类选择器结合使用,例如 :where()、 :not() 和 :is()

结合 `:has()` 和 `:is()`

例如,您可以检查任何 HTML 标题是否至少包含一个 `<a>` 元素作为后代元素。

:is(h1, h2, h3, h4, h5, h6):has(a) {
  color: blue;
}

/* is equivalent to: */
h1:has(a),
h2:has(a),
h3:has(a),
h4:has(a),
h5:has(a),
h6:has(a) {
  color: blue;
}

您也可以将 `:is()` 作为参数传递给 `:has()`。想象一下,如果我们更改了最后一个示例,以便任何包含 `<a>` 子元素或任何带有 `.link` 类的子元素的标题级别都被选中。

:is(h1, h2, h3, h4, h5, h6):has(:is(a, .link)) {
  color: blue;
}

结合 `:has()` 和 `:not()`

我们也可以将 `:has()` 与 `:not()` 选择器结合使用!假设您想在一个 `.card` 元素中添加一个边框,前提是它不包含任何 `<img>` 元素后代。当然可以。

.card:not(:has(img)) {
  border: 1px solid var(--my-amazing-color);
}

这将检查该卡片是否 `:has()` 任何图像,然后说。

嘿,如果你没有找到任何图像,请将这些样式应用于它。

想更疯狂吗?让我们选择任何缺少 ` alt` 文本的图像的 `.post` 元素。

.post:has(img:not([alt])) {
  /* Styles */
}

看到我们在这里做了什么吗?这次,`not` 在 `has` 中。这就是在说。

嘿,如果你发现任何包含没有替代文本的图像的帖子,请将这些样式应用于它们,谢谢。

这可以用来调试缺少 `alt` 属性的图像。

这是我从 Eric Meyer 的视频 中借鉴的另一个示例。假设您想要选择任何只包含 `<img>` 元素的 `<div>`。

div:not(:has(:not(img))) {
  /* Styles */
}

我们在这里说的是。

如果你找到一个 `<div>`,并且里面只有 一个或多个图像,就施展你的魔法吧!

这些是顺序很重要的情况。

请注意,更改选择器的顺序会改变它们所选择的内容。我们讨论了 `:has()` 参数列表的严格性,但在我们已经看过的将 `:has()` 与其他关系伪选择器结合使用的示例中,它甚至更加严格。

让我们看一个例子。

article:not(:has(img)) {
  /* Styles */
}

这匹配任何不包含任何图像的 `<article>` 元素。现在让我们颠倒顺序,使 `:has()` 在 `:not()` 之前。

article:has(:not(img)) {
  /* Styles */
}

现在,我们匹配任何包含 *任何内容* 的 `<article>` 元素,只要其中不包含图像。该文章必须有一个后代元素才能匹配,该后代元素可以是任何东西,但不能是图像。

用例

面包屑分隔符

面包屑 是一种方便的方法,可以显示用户当前位于哪个页面以及该页面在站点地图中的位置。例如,如果您在 About 页面上,您可能会显示一个列表,其中包含一个指向主页的链接项和一个仅指示当前页面的项。

<ol class="breadcrumb">
  <li class="breadcrumb-item"><a href="/">Home</a></li>
  <li class="breadcrumb-item current">About</li>
</ol>

这很棒。但是,如果我们想将其显示为水平列表并隐藏列表编号呢?使用 CSS 很容易。

ol {
  display: flex;
  list-style: none;
}

注意! 设置 `list-style: none` 会阻止 Safari 将元素识别为列表

好的,但现在我们剩下两个相互重叠的列表项。由于我们使用的是 Flexbox,所以可以在它们之间添加 `gap`。

ol {
  display: flex;
  gap: .5rem;
  list-style: none;
}

这当然有帮助。但我们可以通过在它们之间添加一个分隔符来更清晰地区分这两个项目。没什么大不了的。

.breadcrumb-item::after {
  content: "/";
}

但是等等!我们不需要在 `current` 项目之后添加分隔符,因为它总是位于列表的最后,并且没有后续内容。这就是 `:has()` 发挥作用的地方。我们可以使用后续子元素组合器 (~) 查找任何具有 `current` 类的子元素。

.breadcrumb-item:has(~ .current)::after {
  content: "/";
}

好了!

无 JavaScript 的表单验证

正如我们之前了解到的,`:has()` 不接受伪元素,但它确实允许我们使用伪类。我们可以将其用作一种轻量级表单验证,我们通常会使用 JavaScript 来处理。

假设我们有一个需要填写电子邮件的新闻稿注册表单。

<form>  
  <label for="email-input">Add your pretty email:</label>
  <input id="email-input" type="email" required>
</form>

电子邮件是此表单中的必填字段。否则,就没有东西可以提交!也许我们可以添加一个红色边框到输入框中,如果用户输入了无效的电子邮件地址。

form:has(input:invalid) {
  border: 1px solid red;
}

尝试一下。尝试输入一个无效的电子邮件地址,然后按 Tab 键或单击 Password 字段。

在待办事项列表中设置已完成项目的样式

您是否尝试过在选中输入框时设置复选框标签的样式?您知道,就像待办事项列表应用程序,您通过选中一个框来完成列表中的项目。

您的 HTML 结构可能如下所示。

<form>
  <input id="example-checkbox" type="checkbox">
  <label for="example-checkbox">We need to target this when input is checked</label>
</form>

虽然最好将标签放在输入元素之前或将其包裹在输入元素周围,以 提高可访问性,但您必须将标签放在输入元素之后,才能根据输入元素的 `check` 属性来选择标签。

使用下一个兄弟组合器 (+),您可以像这样设置标签的样式。

/* When the input is checked, 
  style the label */
input:checked + label {
  color: green;
}

让我们更改标记并通过将输入元素嵌套在标签中来创建一个 隐式标签

<form>  
  <label>
    <input type="checkbox">
    We need to target this when input is checked
  </label>
</form>

以前,当输入框被选中时,没有办法选择该标签。但现在我们有了 `:has()` 选择器,我们完全拥有了这种能力。

/* If a label has a checked input,
  style that label */
label:has(input:checked) {
  color: green;
}

现在,让我们回到标签在输入框之前的理想标记。

<form> 
  <label for="example-checkbox">We need to target this when input is checked</label>
  <input id="example-checkbox" type="checkbox">
</form>

您仍然可以使用 `:has()` 作为前一个选择器来选择和设置显式标签的样式,同时保持更易访问的标记。

/* If a label has a checked input 
   that is it's next sibling, style 
   the label */
label:has(+ input:checked) {
  color: green;
}
智能的“添加到购物车”按钮

如果我们将 :has() 应用于页面的根元素或 body 元素会发生什么?

:root:has( /* Any condition */ ) {
  /* Styles */
}

:root 是文档的最高级别,它控制着它下面的一切,对吧?如果在 DOM 树的某个很低的位置发生了某些事情,你可以检测到它,并相应地对 DOM 树的另一个分支进行样式设置。

假设你运营着一个电子商务网站,并且你想在将产品添加到购物车时对“添加到购物车”按钮进行样式设置。这很常见,对吧?像亚马逊这样的网站一直这样做,让用户知道商品已成功添加到他们的购物车中。

想象一下,这是我们 HTML 的结构。“添加到购物车”按钮包含在 <header> 元素中,而产品包含在 <main> 元素中。

一个图表,标识产品和按钮项在 DOM 树中的位置。

标记的超简化示例可能如下所示

<body>
  <header>
    <button class="cart-button">Add to cart</button>
  </header>
  <main> 
    <ul>
      <li class="p-item">Product</li>
      <li class="p-item is-selected">Product</li>
      <li class="p-item">Product</li>
    </ul>
  </main>
</body>

在 CSS 中,我们可以检查 <body> 是否 :has() 任何具有 .p-item 和 .is-selected 类的后代。一旦此条件为真,就可以选择 .cart-button 。

body:has(.p-item.is-selected) .cart-button {
  background-color: green;
}
更改颜色主题

深色模式、浅色模式、高对比度模式。为用户提供自定义网站颜色主题的选择,可以成为一个不错的 UX 改进。

假设在文档的某个很深的位置,你有一个 <select> 菜单供用户选择颜色主题

<select>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
  <option value="high-contrast">High-contrast</option>
</select>

我们可以在 <body> 元素上使用 :has() 并检查 <select> 菜单的选定 <option> 。这样,如果 <option> 包含某个值,我们就可以使用不同的颜色值来更新CSS 自定义属性,从而更改当前的颜色主题。

body:has(option[value="dark"]:checked) {
  --primary-color: #e43;
  --surface-color: #1b1b1b;
  --text-color: #eee;
}

同样,某些事情正在发生(用户 <select> 了一个 <option> )在 DOM 树的某个较低位置,而我们在树的最高级别( <body> )监控变化并相应地更新样式(通过自定义属性)。

根据子元素的数量对元素进行样式设置

这是一个巧妙的技巧,来自Bramus Van Damme。 :has() 可以根据父容器中子元素的数量应用样式。

想象一下,你有一个两列布局。如果网格中项目的数量是奇数——3、5、7、9 等——那么在最后一个项目之后,你将不得不在网格中留下一个空白空间。

调整两列布局,其中子元素数量为奇数,第一个子元素跨越第一行。

如果网格中的第一个项目能够占据第一行的两列,以防止这种情况发生,那将更好。为此,你需要检查网格中的 :last-child 元素是否也是一个奇数编号的子元素。

/* If the last item in a grid is an odd-numbered child */
.grid-item:last-child:nth-child(odd) {
  /* Styles */
}

这可以传递到 :has() 参数列表中,因此当 :last-child 为奇数时,我们可以对网格的 :first-child 进行样式设置,使其占据网格的第一行的全部。

.grid:has(> .grid-item:last-child:nth-child(odd)) .grid-item:first-child {
  grid-column: 1 / -1;
}

浏览器支持

此浏览器支持数据来自Caniuse,其中有更多详细信息。数字表示浏览器从该版本开始支持该功能。

桌面

ChromeFirefoxIEEdgeSafari
10512110515.4

移动设备/平板电脑

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712715.4

测试支持

@supports at-规则支持 :has() ,这意味着我们可以检查浏览器是否支持它,并根据结果有条件地应用样式。

@supports(figure(:has(figcaption))) {
  /* Supported! */
}

更多信息