CSS :has() 选择器实战:父元素驱动样式不再靠JS的四个场景

2026-08-11 0 594

我刚接触 :has() 的时候还以为是在开玩笑,毕竟“父母的样式取决于孩子”,这在 CSS 里从来没存在过。但它真成了,而且 Chrome 105、Safari 15.4、Firefox 121 一早已支持。也就是说,2026 年这玩意儿已经可以在生产环境放心用。今天不聊那些理论,直接甩几个我能用上的真实案例。

1. 表单验证的视觉反馈,再也不写 JS 类名

以前做一个必填输入框,校验失败要加个红色边框,通常是在 JS 里 addClass,或者用 input 事件去 toggle。现在有了 :has(),完全可以让浏览器自己判断。

看看这段 HTML:

<div class="field">
  <label for="name">用户名</label>
  <input id="name" required placeholder="至少3个字符" pattern=".{3,}">
</div>

然后配合 CSS,只要输入框非法,整个表单字段的边框和标签都变红;输入合法后就变成绿色的。

/* 普通状态 */
.field {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 6px;
  transition: border-color 0.2s;
}

/* 当子元素 input 值非法时,给 .field 加个红边 */
.field:has(input:invalid) {
  border-color: #e74c3c;
}

/* 当 input 值合法时,加绿边 */
.field:has(input:valid) {
  border-color: #2ecc71;
}

/* 顺手把 label 也换颜色 */
.field:has(input:invalid) label {
  color: #e74c3c;
}
.field:has(input:valid) label {
  color: #2ecc71;
}

注意 :valid 对空文本但带 required 的输入框是不成立的,所以未填写时是非法状态,体验没问题。复杂的表单校验可以继续组合伪类,完全不用动 JS。

2. 卡片悬停互动,兄弟元素优雅退让

很多 UI 里有这种效果:鼠标悬浮在一张卡片上,其他卡片自动降低透明度。以前要监听 mouseenter 然后给父容器加类名,现在代码短到离谱。

假设卡片组结构:

<div class="card-group">
  <div class="card">内容 A</div>
  <div class="card">内容 B</div>
  <div class="card">内容 C</div>
</div>

写这么几行就有那种“鼠标指着谁谁清晰,别人变得透明”的互动体验:

.card-group:has(.card:hover) .card:not(:hover) {
  opacity: 0.6;
  transform: scale(0.98);
  filter: grayscale(30%);
  transition: all 0.2s;
}

.card-group {
  display: flex;
  gap: 16px;
}

.card {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 12px;
}

简单说:当 .card-group 的下一层里存在一个 hover 状态的 .card 时,就把所有没有 hover 的 .card 变淡。这比老办法减少了好几行 JavaScript,而且不会干扰触屏设备的点击事件。

3. 子元素数量变化,动态切换布局

有时候列表里的元素数量会随机变化,比如购物车里的优惠券数量从 1 变成 5。如果数量太少一行平铺很清爽,数量多了就自动变成两列。用媒体查询显然做不到,因为容器宽度是固定的,只有内容数量在变。

这里就是 :has():nth-child() 配合的高光时刻。

<div class="tag-list">
  <span class="tag">React</span>
  <span class="tag">Vue</span>
  <span class="tag">Angular</span>
  <span class="tag">Svelte</span>
  <span class="tag">Solid</span>
</div>

CSS 里判断标签数大于等于 5 时,让列表变成网格布局,每个标签占两列。少于 5 个就让它们横着排。

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 根据子元素数量改变父容器布局 */
.tag-list:has(> .tag:nth-child(n+5)) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.tag-list:has(> .tag:nth-child(n+5)) .tag {
  text-align: center;
}

.tag {
  background: #eef2ff;
  padding: 6px 12px;
  border-radius: 20px;
}

原理是 :has() 里用了 > .tag:nth-child(n+5),只要存在第 5 个 .tag,整个父级就切换成网格。这种条件完全由内容自己驱动,放在组件里特别合适。

4. 与容器查询一起用,做组件级响应

容器查询(Container Queries)允许你在组件内部根据父容器的宽度调整样式。但有时候,你想根据“组件里有没有某个特殊元素”来变化,比如某个卡片组件里可能有一个“推荐”徽章,有徽章时卡片标题要大一点,或者某个插图要放在右侧。两种能力叠加,代码会很清爽。

先给容器设定查询条件:

<div class="product-card">
  <div class="badge">限时秒杀</div>
  <h2 class="title">无线降噪耳机</h2>
  <a class="buy-now">立刻购买</a>
</div>
.product-card {
  container-type: inline-size;
  /* 其他样式 */
}

/* 默认标题字号 */
.product-card .title {
  font-size: 1.2rem;
}

/* 当容器宽度较窄时,标题变小 */
@container (max-width: 300px) {
  .product-card .title {
    font-size: 1rem;
  }
}

/* 注意:有徽章时,即使容器再宽,标题也强制限制大小 */
.product-card:has(.badge) .title {
  font-size: 1rem;
  color: #c0392b;
}

/* 有徽章且容器很窄时,间距也调整 */
.product-card:has(.badge) .buy-now {
  margin-top: 20px;
  width: 100%;
}

这个例子里,:has() 起到了“特性开关”的作用——组件有某个子元素,它的样式就能按另一种规则走。这和 JS 里的“条件渲染”很像,但代价为零。

结语

我并没有把 :has() 所有用法都列出来,只拿出我觉得真正能少写几行 JS 的四个方向。它最让人上瘾的地方不是能替代 JS,而是让你用 CSS 自己的方式去表达“父子关系”。其实很多交互都不用动 DOM 了,把状态弄到 class 里,然后让 :has() 自己去判断,代码会突然变得很纯粹。

如果非要说坑,那就是性能。过度使用复杂的选择器可能会让重排变慢,但日常场景远远没到那个程度。放心用它,至少可以让你的 CSS 文件里少十几个类名。

CSS :has() 选择器实战:父元素驱动样式不再靠JS的四个场景
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css CSS :has() 选择器实战:父元素驱动样式不再靠JS的四个场景 https://www.taomawang.com/web/css/2525.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务