[译] 通过示例学习 CSS :has() 选择器: 5 个顶级使用案例

CSS中的 :has 选择器打开了一个充满新可能性的世界。现在它已经登陆 Firefox 121,自此所有现代浏览器都支持它

在本篇博客中,我们将通过一些示例深入了解 :has 的实用性。和我一起探索这个 CSS 选择器的强大功能吧!

1. 父级选择器

:has() 选择器的主要用途是作为父选择器。它有用于检查父元素是否存在某个元素。

例如,考虑一个场景,你想验证按钮中是否存在图标:

css 复制代码
button:has(.icon) {
    display: flex;
    gap: 10px;
}

或者,如果你想在包含子菜单的导航栏项上附加下拉箭头,也可以这样做:

css 复制代码
nav li:has(ul) > a::after {
    content: "+";
    margin-inline: 10px;
}

我们搜索列表(ul)中的列表项(li),一旦找到,我们就选择链接(a)并加上内容为加号的伪元素,这里有个在线例子

:has() 选择器的运行方式与选择器链中的任何其他选择器一样。 您可以在其之前、之后或内部包含任何内容。例如,当 .card 元素包含的 <img>, 同时<img>后紧跟着 <p> 时,定位该元素:

css 复制代码
.card:has(img + p) {
    flex-direction: row;
}

这样,我们就可以根据子元素的情况为卡片创建自适应布局:

在线例子

您还可以在 :has() 内提供用逗号分隔的元素列表,检查它们是否在父元素中:

css 复制代码
article:has(video, iframe) {
  /* 匹配至少包含一个 video 或 iframe 的 <article> */
}

这可确保 article 至少具有其中一个子元素 - <iframe><video>

此外,您可以将 :has():not() 结合使用以实现更复杂的场景:

css 复制代码
.card:not(:has(img)) {
  /* 匹配不包含 img 的 .card 元素 */
}

.post:has(img:not([alt])) {
  /* 匹配包含没有 alt 属性的 img 为子元素 的 .post 元素 */
}

div:not(:has(:not(img))) {
  /* 匹配只有 img 子元素的 div 元素 */
}

正如你所看到的,人们可以使用此选择器让 CSS 变得非常有创意,但请记住,不能嵌套 :has() 选择器,但是,你可以链式使用它们:

css 复制代码
.headings:has(.subtitle:has(h2)) {
  /* 无效 */
}

.headings:has(h2):has(.subtitle) h2 {
  /* 有效 */
}

最后,还可以检查子元素的状态,以进行表单验证等操作:

css 复制代码
form:has(input:invalid) {
    border: 1px solid red;
}

您可以查看下面例子:

在线例子

2. 向前兄弟选择器

:has() 选择器不仅限于针对父元素; 它还可以选择前一个的兄弟元素。

例如,你可以根据标签的下一个相邻兄弟元素(如复选框)的状态来设置标签的样式:

html 复制代码
<label for="checkbox">We need to target this when input is checked</label>
<input id="checkbox" type="checkbox">

你的 CSS 中:

css 复制代码
label:has(+ input:checked) {
  color: green;
}

该 CSS 选择器的目标是 <label> 元素,<label> 其后紧跟着选中的 <input> 元素。

我还在 CodePen 上为这个示例添加了一个示例:

在线例子

我们并不局限于只选择前一个兄弟节点,我们还可以选择所有前面的兄弟节点。考虑一下有面包屑分隔符的情况。在这种情况下,我们不希望在列表中的最后一个项目后出现任何分隔符,因此我们可以使用 :has() 和同辈分隔符 (~) 来实现这一功能:

css 复制代码
.breadcrumb-item:has(~ .current)::after {
    content: "/";
}

顺便说一句,这个例子是我从 Eric Meyer 那里学来的。

它针对的是 .breadcrumb-item 元素,这些元素的后面同级元素中有 .current 元素,而不一定是直接相邻的元素。

但是,如果我们不使用 .current 类,而只想根据 HTML 标签查找元素呢?你也可以使用 :has() 来实现这一目的:

css 复制代码
li:has(~ li:not(a))::after {
    content: "/";
    margin-inline-start: 10px;
}

我们要选择的列表项满足:后面同级列表项中有一个没有 <a> 标签的列表项。

对于本小节中的最后一个示例,让我们来分析一下如何使用我们 has 实现以下效果。

在线示例

我第一次看到这种技术是在 Chris Coyier 的 CodePen 上

这个操作的目的是在被悬停的元素之后和之前各选择两个元素,并为它们添加变换效果。

css 复制代码
li:has(+ li + li:hover) {
    transform: scaleY(1.1);
}

li:has(+ li:hover) {
    transform: scaleY(1.2);
}

li:hover {
    transform: scaleY(1.3);
    opacity: 1;
}

li:hover + li {
    transform: scaleY(1.2);
}

li:hover + li + li {
    transform: scaleY(1.1);
}

如果你想看看这个用法的另一个很酷的实用示例,请查看 Stephanie 在 CodePen 上的星级评定组件

3. 数量查询

:has() 选择器引入了在 CSS 中执行数量查询的功能,让你可以根据父元素的子元素数量为其设计样式。

以下是我从 Bramus 的精彩博文中摘录的一些例子。

css 复制代码
/* 至多 3 个子元素,包括 0 个 */
ul:has(> :nth-child(-n+3):last-child) {
    outline: 1px solid red;
}

/* 至多 3 个子元素,包括 0 个 */
ul:not(:has(> :nth-child(3))) {
	outline: 1px solid red;
}

/* 5 个子元素 */
ul:has(> :nth-child(5):last-child) {
	outline: 1px solid blue;
}

/* 至少 10 个元素 */
ul:has(> :nth-child(10)) {
	outline: 1px solid green;
}

/* 7 个到 9 个子元素 */
ul:has(> :nth-child(7)):has(> :nth-child(-n+9):last-child) {
	outline: 1px solid yellow;
}

我在 Twitter 上看到过一个实用的例子。假设你为一个表格设置了最大高度,但希望在表格有更多数据(特别是达到 5 行时)时取消该限制:

css 复制代码
.table-wrapper:has(tr:nth-child(5)) {
    max-height: none;
}

如果你想体验一下,这里有示例

该示例展示了如何根据元素内部内容的多少动态更改样式。

4. 任意位置选择器

当与 htmlbody 或组件根等顶层元素一起使用时,:has() 选择器允许你根据这些元素中的特定条件应用样式,从而为你提供了广泛的可能性。

例如,您可以根据文档中的 <select> 元素切换主题:

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

简单来说,我们要检查我们的 body 元素是否选中了值为 "dark" 的 option 元素。然后,我们更新颜色变量。这也适用于浅色和高对比度主题模式。你可以在下面的演示中看到实际操作:

在线示例

你也可以使用相同的技术来更改布局。例如,假设你要在 body 中查找一个带有选中 "list" 值的单选 input,并将特定样式应用到 DOM 中的某个卡片列表:

css 复制代码
body:has(input[value="list"]:checked) .card-list {
    grid-template-columns: 1fr;
}

在线例子

此外,我最近还在 Rob Bowen 的文章中看到了另一个实用的例子。它涉及在打开弹窗时锁定滚动条。通过检查 body 是否包含作为子元素的弹窗,可以更改 bodyoverflow 属性。

css 复制代码
body:has(.modal) {
    overflow: hidden;
}

5. 除我之外的所有选择器

:has() 实现的 all-but-me 选择器可以选择除被交互元素以外的所有元素。例如,当鼠标悬停在一个子元素上时,您可以选择除被悬停元素以外的所有同级元素。查看此示例来看是如何实现的。

这是选择器:

css 复制代码
.card-list:has(.card:hover) .card:not(:hover) {
    filter: blur(4px)
}

该选择器的内容如下:当容器的子元素卡片悬停时,选择所有未悬停的卡片。

最后一个示例到此结束。我已将所有这些示例添加到我的 CodePen 账户上的一个集合中。我计划今后加入更多示例,并不断更新该集合。

总结

重要的是要注意不要过度依赖 :has 这样的选择器。虽然选择器功能强大,但过度使用可能会让你忽略更简单的选择器写法。取得平衡对于编写易于维护的代码至关重要!

如果你想进一步了解 :has 选择器,我的朋友 Geoff 和我在 CSS-Tricks 上写了一篇博文。欢迎阅读,了解更多信息!

相关推荐
10年前端老司机15 分钟前
什么!纯前端也能识别图片中的文案、还支持100多个国家的语言
前端·javascript·vue.js
摸鱼仙人~18 分钟前
React 性能优化实战指南:从理论到实践的完整攻略
前端·react.js·性能优化
程序员阿超的博客1 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2451 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇6 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖6 小时前
http的缓存问题
前端·javascript·http
小小小小宇7 小时前
请求竞态问题统一封装
前端
loriloy7 小时前
前端资源帖
前端
源码超级联盟7 小时前
display的block和inline-block有什么区别
前端
GISer_Jing7 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js