从一开始(用 CSS 的术语来说),我们一直使用各种意义上的级联。我们的样式可组成“层叠样式表”。我们的选择器也会进行级联。它们可能会横着走。在大多数情况下,它们是向下的。但永远不要朝上。多年来,我们一直梦想着使用“父级选择器”。现在终于来了!采用 :has()
伪选择器的形状。
如果作为参数传递的任何选择器与至少一个元素匹配,则 :has()
CSS 伪类代表相应元素。
但它不仅仅是一个“家长”选择器。这是一种很好的营销方式。不太吸引人的方式可能是“视情况环境”选择器。但这次的圆环并不相同。“家庭”怎么样选择器?
浏览器支持
在继续深入讨论之前,有必要介绍一下浏览器支持。还没有达到目标。但距离实现目标更近了。尚不支持 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()
带来了很多机会。甚至可能尚未被发现。考虑下面这些挑战。
选择具有直接 figcaption
的 figure
元素。
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) { … }
选择 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-media
或 card--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()
显示或隐藏字段的错误消息。选取我们的“email”字段组,在其中添加一条错误消息。
<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-expanded
为 true
时,使用 :has()
检测这一点并更新滑动导航的样式。JavaScript 负责执行自己的操作,而 CSS 可以根据该信息执行所需的操作。无需重排标记,也无需添加额外的类名称等(注意:这并非可正式投入使用的示例)。
:root:has([aria-expanded="true"]) {
--open: 1;
}
body {
transform: translateX(calc(var(--open, 0) * -200px));
}
能否帮助避免用户错误?
所有这些示例有何共性?除此之外,它们展示了 :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)) { … }
- 限制在伪元素之后使用
:has()
css ::part(foo):has(:focus) { … }
- 如果使用
:visited
,则始终为 falsecss :has(:visited) { … }
如需了解与 :has()
相关的实际性能指标,请查看此 Glitch。感谢 Byungwoo 与我们分享有关植入的这些数据洞见和细节。
大功告成!
准备好迎接 :has()
。与朋友分享并分享这篇博文,它将彻底改变我们处理 CSS 的方式。
所有演示均可在此 CodePen 集合中找到。