使用 :has() 作为 CSS 父选择器及其他更多内容

前端开发人员一直梦想有一种方法,可以根据元素内部的情况将 CSS 应用到该元素上。

也许我们想在顶部有英雄图像的情况下对文章元素应用一种布局,而在没有英雄图像的情况下应用另一种布局。又或者,我们想根据表单输入栏的状态为表单应用不同的样式。如果侧边栏中有某个组件,就给该侧边栏一种背景颜色,如果没有该组件,就给该侧边栏另一种背景颜色,怎么样?诸如此类的用例由来已久,网络开发人员曾多次找到 CSS 工作组,恳求他们发明一种 “父选择器”。

在过去的二十年中,CSS 工作组多次讨论了这种可能性。需求是明确的,也是众所周知的。定义语法是一项可行的任务。但如何让浏览器引擎处理可能非常复杂的循环模式,并快速完成计算,这似乎是不可能的。我们为 CSS3 起草了父选择器的早期版本,但却被推迟了。最后,:has() 伪类在 CSS 选择器 level 4中被正式定义。但仅有网络标准并不能让 :has() 成为现实。我们仍然需要一个浏览器团队来解决性能方面的实际挑战。与此同时,计算机的功能一年比一年强大,速度一年比一年快。

2021 年,Igalia 开始在浏览器工程团队中倡导 :has()提出他们的想法并记录他们在性能方面的发现。对 :has() 的重新关注引起了苹果公司 WebKit 工程师的注意。我们开始实施这个伪类,并思考如何提高性能以实现这一目标。我们争论的焦点是,是先开发一个速度更快、功能非常有限且范围很窄的版本,然后尽可能消除这些限制……还是先开发一个没有限制的版本,只在需要时施加限制。我们选择了后者,并实现了功能更强大的版本。我们开发了许多新颖的 :has()特定缓存和过滤优化功能,并充分利用了 CSS 引擎现有的高级优化策略。我们的方法取得了成功,证明了经过二十年的等待,我们终于可以实现这样一个性能卓越的选择器,即使在存在大型 DOM 树和大量 :has() 选择器的情况下也是如此。

WebKit 团队于 2021 年 12 月在 Safari 技术预览版 137 中推出了 :has(),并于 2022 年 3 月 14 日在 Safari 15.4 中推出了 :has()。Igalia 负责在 Chromium 中实现 :has() 的工程设计工作,Chromium 将于 2022 年 8 月 30 日在 Chrome 105 中发布 :has()。据推测,基于 Chromium 开发的其他浏览器也不会落后太多。Mozilla 目前正在开发 Firefox 的实现方法。

那么,让我们一步步来亲身体验一下,网络开发人员可以利用这个渴求已久的工具做些什么。事实证明,:has() 伪类不仅仅是一个 “父选择器”。在经历了几十年的死胡同之后,这个选择器能做的事情远不止这些。

如何使用 :has() 作为父选择器的基础知识

让我们从最基本的开始。想象一下,我们想根据图中内容的类型来样式化 <figure> 元素。有时,我们的图表只包含一张图片。

<figure>
  <img src="flowers.jpg" alt="spring flowers">
</figure>

而其他时候则是一张图片加一个说明。

<figure>
  <img src="dog.jpg" alt="black dog smiling in the sun">
  <figcaption>Maggie loves being outside off-leash.</figcaption>
</figure>

现在,让我们为figure应用一些样式,这些样式只有在数字内部有 figcaption 时才会应用。

figure:has(figcaption) {
  background: white;
  padding: 0.6rem;
}

这个选择器的意思是–任何内含 figcaptionfigure 元素都会被选中。

如果你想修改代码,看看会发生什么,这里有一个演示。请务必使用支持 :has() 的浏览器,目前支持 Safari。

在这个演示中,我还使用 figure:has(pre),针对任何包含 pre 元素的figure 。

figure:has(pre) { 
  background: rgb(252, 232, 255);
  border: 3px solid white;
  padding: 1rem;
}

只要当前浏览器支持 :has(),我就会使用选择器功能查询来隐藏浏览器支持提醒。

@supports selector(:has(img)) {
  small {
    display: none;
  }
}

@supports selector() at-规则 本身就得到了很好的支持。如果您想使用特性查询来测试浏览器对特定选择器的支持,它就会非常有用。

最后,在第一个演示中,我还使用 :not() 伪类编写了一个复杂的选择器。我想在 figure 中应用 display: flex,但前提是图片是唯一的内容。Flexbox 可以拉伸图片,使其填满所有可用空间。

我使用了一个选择器来针对没有任何非 img 元素的任何figure 。如果图中有 figcaptionpreph1,或者除了 img 之外的任何元素,那么选择器就不适用。

figure:not(:has(:not(img))) {
  display: flex;
}

:has() 功能强大。

在 CSS Gridv中使用 :has() 的实用示例

让我们来看第二个演示,在这个演示中,我使用 :has() 作为父选择器,轻松解决了一个非常实际的需求。

我使用 CSS Grid 制作了几张文章预告卡片。有些卡片只包含标题和文字,而有些卡片还包含图片。我希望有图片的卡片比没有图片的卡片占用更多的网格空间。

我不想做额外的工作,让内容管理系统应用类或使用 JavaScript 进行布局。我只想在 CSS 中编写一个简单的选择器,告诉浏览器让任何带图片的预告卡在网格中占据两行两列。

:has()伪类让这一切变得简单:

article:has(img) {
  grid-column: span 2;
  grid-row: span 2;
}

前两个演示使用的是 CSS 早期的简单元素选择器,但所有选择器都可以与 :has() 结合使用,包括类选择器、ID 选择器、属性选择器以及功能强大的组合器。

使用 :has() 和子组合器

首先,简要回顾一下子代组合器和子组合器 (>) 之间的区别。

子代组合器从 CSS 诞生之初就存在了。它是我们在两个简单的选择符之间加上空格的别称。就像这样:

a img { ... }

这将针对包含在 a 元素中的所有 img 元素,无论 a img 在 HTML DOM 树中相距多远。

<a>
  <figure>
    <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
  </figure>
</a>

子组合器是我们在两个选择器之间添加 > 时使用的名称,它告诉浏览器将任何与第二个选择器相匹配的内容作为目标,但只有当第二个选择器是第一个选择器的直接子代时才会这样做。

a > img { ... }

例如,该选择器的目标是所有被 a 元素包装的 img 元素,但只有当 img 紧跟在 HTML 中的 a 元素之后时才会出现。

<a>
  <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
</a>

有鉴于此,让我们来看看下面两个示例之间的区别。这两个例子都选择了 a 元素,而不是 img,因为我们使用了 :has()

a:has(img) { ... }
a:has(> img) { ... }

第一种方法会选择 HTML 结构中任何一个内含 imga 元素。而第二种方法只有在 imga 的直接子元素时才会选择该元素。

这两种方法都很有用,但它们实现的目标不同。

还有两种组合器,它们都是同级组合器。通过这两种组合器,:has() 不仅仅是一个父选择器。

使用 :has() 和同级组合器

让我们回顾一下这两个具有同级关系的选择器。它们是下一个紧邻兄弟组合器 (+) 和后续同辈组合器 (~)。

紧邻兄弟组合器 (+) 只选择直接位于 h2 元素之后的段落。

h2 + p
<h2>Headline</h2>
<p>Paragraph that is selected by `h2 + p`, because it's directly after `h2`.</p>

后续同级组合器 (~) 会选择 h2 元素之后的所有段落。它们必须是同级元素,但中间可以有任意数量的其他 HTML 元素。

h2 ~ p
<h2>Headline</h2>
<h3>Something else</h3>
<p>Paragraph that is selected by `h2 ~ p`.</p>
<p>This paragraph is also selected.</p>

请注意,h2 + ph2 ~ p 选择的都是段落元素,而不是 h2 标题。与其他选择器一样(想想 img),选择器的目标是最后列出的元素。但如果我们想选择 h2 呢?我们可以使用同级组合器 :has()

你是否经常想根据标题后面的元素调整标题的边距?现在变得简单了。通过这段代码,我们可以选择任何一个后面紧跟着 ph2

h2:has(+ p) { margin-bottom: 0; }

太神奇了

如果我们想对所有六个标题元素都这样做,又不想写出六份选择器,该怎么办呢?我们可以使用 :is 来简化代码。

:is(h1, h2, h3, h4, h5, h6):has(+ p) { margin-bottom: 0; }

或者,如果我们要为更多元素编写这段代码,而不仅仅是 paragrapahs 呢?只要标题后面有段落、标题、代码示例和列表,我们就可以消除所有标题的下边距。

:is(h1, h2, h3, h4, h5, h6):has(+ :is(p, figcaption, pre, dl, ul, ol)) { margin-bottom: 0; }

:has() 后代组合器子代组合器 (>)、紧邻兄弟组合器 (+) 和后续同胞组合器 (~) 结合使用,会带来无限可能。不过,这还仅仅是个开始。

不使用 JS 的表单状态样式

:has() 中可以使用很多奇妙的伪类。事实上,它彻底改变了伪类的功能。以前,伪类只用于根据特殊状态对元素进行样式化,或对其某个子元素进行样式化。现在,伪类可以用来捕捉状态,而无需 JavaScript,并根据该状态为 DOM 中的任何元素设计样式。

表单输入字段为捕获这种状态提供了一种强大的方法。特定于表单的伪类包括::autofill:enabled:disabled:read-only:read-write:placeholder-shown:default:checked:indeterminate:valid:invalid:in-range:out-of-range:required:optional

让我们来解决我在引言中描述的用例之一–长期以来一直需要根据输入字段的状态来样式化表单标签。让我们从一个基本表单开始。

<form>
  <div>
    <label for="name">Name</label> 
    <input type="text" id="name">
  </div>
  <div>
    <label for="site">Website</label> 
    <input type="url" id="site">
  </div>
  <div>
    <label for="email">Email</label>
    <input type="email" id="email">
  </div>
</form>

我想在某个字段处于焦点时,为整个表单应用背景。

form:has(:focus-visible) { 
  background: antiquewhite;
}

现在,我本可以使用 form:focus-within 代替 ,但它的行为就像 form:has(:focus)。只要字段处于焦点状态,:focus 伪类就会应用 CSS。:focus-visible 伪类提供了一种可靠的方法,只有在浏览器会本地绘制焦点指示器时,才会对其进行样式化,它使用了与浏览器用来确定是否应用焦点环相同的复杂启发式方法

现在,假设我想为其他字段(非焦点字段)设计样式–改变它们的标签文本颜色和输入边框颜色。在使用 :has() 之前,这需要 JavaScript。现在我们可以使用 CSS。

form:has(:focus-visible) div:has(input:not(:focus-visible)) label {
  color: peru;
}
form:has(:focus-visible) div:has(input:not(:focus-visible)) input {
  border: 2px solid peru;
}

这个选择器是怎么用的?如果此表单中的某个控件有焦点,而此表单控件的输入元素没有焦点,则将此标签文字的颜色改为peru色。并将输入框的边框改为 2px solid peru

在下面的演示中,点击其中一个文本字段,就能看到这段代码的实际效果。如前所述,表单的背景会发生变化。非焦点字段的标签和输入边框颜色也会发生变化。

在同一个演示中,我还想改进当用户填写表格出错时的警告功能。多年来,我们一直可以使用 CSS 在无效输入周围添加一个红框。

input:invalid {
  outline: 4px solid red;
  border: 2px solid red;
} 

现在,通过 :has(),我们还可以将标签文本变为红色:

div:has(input:invalid) label {
  color: red;
}

在网站或电子邮件字段中输入非完整格式的 URL 或电子邮件地址,就能看到结果。两者都是无效的,因此都会触发红色边框和红色标签,并带有一个 “X”。

无 JS 的暗模式切换

最后,在同一个演示中,我使用一个复选框让用户在浅色和深色主题之间切换。

body:has(input[type="checkbox"]:checked) {
  background: blue;
  --primary-color: white;
}
body:has(input[type="checkbox"]:checked) form { 
  border: 4px solid white;
}
body:has(input[type="checkbox"]:checked) form:has(:focus-visible) {
  background: navy;
}
body:has(input[type="checkbox"]:checked) input:focus-visible {
  outline: 4px solid lightsalmon;
}

我使用自定义样式设计了暗模式复选框的样式,但它看起来仍像一个复选框。如果使用更复杂的样式,我可以在 CSS 中创建一个切换框

同样,我也可以使用选择菜单为用户提供网站的多个主题

body:has(option[value="pony"]:checked) {
  --font-family: cursive;
  --text-color: #b10267;
  --body-background: #ee458e;
  --main-background: #f4b6d2;
}

只要有机会使用 CSS 而不是 JavaScript,我都会抓住。这会带来更快的体验和更强大的网站。JavaScript 可以做很多了不起的事情,当它是最合适的工具时,我们就应该使用它。但是,如果我们只用 HTML 和 CSS 就能达到同样的效果,那就更好了。

更多

纵观其他伪类,有很多都可以与 :has() 结合使用。想象一下:nth-child:nth-last-child:first-child:last-child:only-child:nth-of-type:nth-last-of-type:first-of-type:last-of-type:only-of-type 的可能性。全新的 :modal 伪类会在dialog处于打开状态时触发。使用 :has(:modal) 可以根据dialog是打开还是关闭来样式化 DOM 中的任何内容。

不过,目前并非每个浏览器都支持 :has() 内的每个伪类,因此请在多个浏览器中试用您的代码。目前,动态媒体伪类不起作用,如:playing:paused:muted 等。这些伪类将来很可能会工作,所以如果您将来正在阅读此文,请测试一下!此外,在某些特定情况下,表单失效支持目前缺失,因此这些伪类的动态状态更改可能无法通过 :has() 更新。

Safari 16 将添加对 :has(:target) 的支持,这为编写代码提供了有趣的可能性,可以在当前 URL 中查找与特定元素 ID 匹配的片段。例如,如果用户点击了文档顶部的目录,并跳转到与该链接匹配的页面部分,:target 提供了一种方法,可根据用户点击链接到达的事实,对该内容进行唯一样式化。而 :has() 则为这种样式化提供了可能。

需要注意的是,CSS 工作组决定禁止在 :has() 中使用所有现有的伪元素。例如,article:has(p::first-line)ol:has(li::marker) 将不起作用。::before::after也一样。

:has()革命

这感觉就像是我们编写 CSS 选择器方式的一场革命,为我们打开了一个以前不可能实现或通常不值得付出努力的可能性世界。虽然我们可能会立即意识到 :has() 会有多有用,但我们也不知道真正的可能性有多大。在接下来的几年中,制作演示并深入研究 CSS 功能的人会提出令人惊叹的想法,将 :has() 的功能发挥到极致。

Michelle Barker 制作了一个奇妙的演示,通过使用 :has() 和悬停状态来触发网格轨道尺寸的动画。请在她的博文中阅读更多相关信息。Safari 16 将支持动画网格轨迹。您现在就可以在 Safari 技术预览版或 Safari 16 测试版中试用此演示。

:has()最难的部分是让我们对它的可能性敞开心扉。我们已经习惯了没有父选择器给我们带来的限制。现在,我们必须打破这些习惯。

因此,我们更有理由使用 vanilla CSS,而不是将自己局限于框架中定义的类。通过编写自己的 CSS,为自己的项目定制 CSS,你可以充分利用当今浏览器的所有强大功能。

你将如何使用 :has()?去年 12 月,我在 Twitter 上询问大家 :has() 有哪些用例,收到了很多回复,其中不乏令人难以置信的想法。我迫不及待地想看看你的想法。

本文文字及图片出自 Using :has() as a CSS Parent Selector and much more

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注