当CSS学会向上看::has() 伪类让表单反馈和卡片布局变得像变魔术

2026-08-07 0 764

以前写CSS,总有种被“祖先选择器”压着抬不起头的感觉。你想根据子元素的状态去控制父元素的样式,只能给父元素加class,或者用JS在事件里来回加。2023年之前,我基本默认“样式只能向下追溯”。但自从各大浏览器都支持了:has()之后,我发现CSS终于开始向上看了。

很多人认识:has()是从“父选择器”这个外号开始的,但我觉得它的能力远远不止“选父元素”。如果你只拿它来高亮一个有图片的卡片,那实在浪费。今天我用几个实战片段,带你看看怎么用:has()解决一些以前必须写JS的交互问题。保证每一个案例你都能用到实际项目里。

一、记一次和:has()的初遇

第一次在同事代码里看到.card:has(img)这种写法,我愣了一下。因为.card后面不是跟空格,不是跟>,而是冒号加has括号,里面又是一个选择器。我下意识觉得“这莫非是jQuery的has?” 试一下发现,它真的就是“如果这个元素内部有img,就选中这个元素本身”。

于是那天下午我重构了一个产品列表页。原来的列表有统一高度的图片占位,有些卡片没有图片,CSS里我写了一大堆.card.no-image这种类名。重构完以后,我把类名全删了,直接:

.product-card:has(.thumbnail) { padding-bottom: 0; }

没有了图片的卡片自动就去掉了多余的内边距。整个代码清爽到同事以为我把逻辑搬到后端去了。

二、最实用的案例:表单输入反馈

HTML5有:required:valid:invalid,但以前你很难把这些状态作用到整个表单项或者父容器上。比如用户在一个输入框里填了错误的内容,你希望整个输入框的外边框变红,同时旁边显示一个错误图标。以前你要写JS监听input事件,或者用兄弟选择器,但错误提示和输入框往往是相隔不定的。

现在用:has(),你可以这样:

.field:has(input:invalid) {
    border: 1px solid #e74c3c;
}

.field:has(input:valid) {
    border: 1px solid #27ae60;
}

配合HTML结构:

<div class="field">
    <label for="email">邮箱</label>
    <input type="email" id="email" required>
    <span class="error-msg">邮箱格式不对</span>
</div>

当输入的内容合法,边框变绿;非法则变红。你可能会问:那错误信息怎么让它显示出来?用:has()也可以控制兄弟元素。这里需要一点小技巧,我们可以把错误信息藏在某处,利用:has()配合sibling?但:has()不能直接影响兄弟,只能影响容器。所以更常见的做法是让错误信息存在于容器内,但默认隐藏,容器有invalid状态时显示:

.field .error-msg {
    display: none;
}

.field:has(input:invalid:not(:placeholder-shown)) .error-msg {
    display: block;
}

注意这里用到了:not(:placeholder-shown),意思是用户输入了内容但非法,才显示错误提示,避免用户一进页面就满屏红。

如果你不想依赖父级,也可以把错误信息放后面,但CSS只能影响当前元素和后代,以及后来的一些伪类。用:has()还是最舒服的。

三、卡片列表的“有图”和“无图”布局

产品列表、文章列表经常会出现“有些卡片有大图,有些卡片只有文字”的情况。以前我们给卡片加个类名,或者用JS检测图片是否存在。现在可以直接用:has(img)来决定卡片是横向布局还是纵向布局。

.card {
    display: flex;
    flex-direction: column;
}

.card:has(> img) {
    flex-direction: row;
    align-items: center;
}

.card:has(> img) .card-body {
    padding-left: 16px;
}

这样的话,有图片的卡片图片在左边,纯文字的卡片内容在下方。无论从后端返回的数据怎么变化,前端样式都能自动适配。我看过很多项目里为了这个效果特意在接口里返回了一个hasImage的布尔值,其实CSS自己就能判断。

如果你觉得这样误判可能性大(比如卡片里有装饰图但不算主图),你可以用更精确的子选择器,例如.card:has(.cover-image),这样就只认有封面的卡片。

四、根据子元素数量动态调整网格列数

以前要实现“如果列表里只有两个子元素,就让它们各占一半宽度;如果有三个,就各占三分之一”,我们需要用JS监听子元素数量,然后添加类名。现在很多新项目用容器查询解决了,但也不完全一样。

:has()可以和:nth-child等结合,实现简单的数量感知。比如一个父容器,里面直接子元素数量是2个还是3个,我们可以这样写:

.list {
    display: grid;
    grid-template-columns: 1fr;
}

.list:has(> :nth-child(2):last-child) {
    grid-template-columns: 1fr 1fr;
}

.list:has(> :nth-child(3):last-child) {
    grid-template-columns: 1fr 1fr 1fr;
}

这里面的妙处是选择器:last-child表示“当前节点是最后一个子元素”。当集合里恰好有2个元素时,第2个元素既是第2个也是最后一个,于是匹配。所以:has(> :nth-child(2):last-child)就表示“子元素数量正好为2”。同理,3个也一样。

这种写法在标签列表、商品sku选择等场景里很实用。你不用再给容器写data-count属性,CSS自己看得懂。

五、菜单状态:一个被忽略的好例子

很多导航菜单的展开状态还是靠JS,或者靠:focus-within。但:focus-within只能判断“内部是否有焦点”,想要根据内部某个特定子元素的展开状态来改变父级信息,用:has()更精准。

比如一个顶部的导航项,里面有一个下拉按钮,还有一个子菜单列表。当下拉按钮被激活(比如用:focus或者aria-expanded属性),我们希望整个导航项高亮。

.nav-item:has(button[aria-expanded="true"]) {
    background: #f0f0f0;
    font-weight: bold;
}

注意这里没用:focus-within,因为如果点击的是外部区域,焦点不在里面,但属性可能还是true。属性选择器靠的是状态,和焦点无关,很适合这种场景。

同样的,你可以写一个纯CSS的折叠面板,不用JS控制打开关闭:

.panel:has(input:checked) .panel-content {
    display: block;
}

配合一个隐藏在标题里的复选框,就可以实现手风琴效果了。这种玩法很有怀旧风格,但确实能说明:has()能读取表单状态。

六、把:has()和容器查询放在一起,威力更大

容器查询(container query)允许你根据容器的宽度来改变样式,但它无法感知容器里面的内容结构。而:has()恰好可以弥补这一点。你可以根据容器内部是否有某个元件,来决定容器的显示逻辑,然后再次应用容器查询。

举个例子,一个新闻卡片组件,可以放在侧边栏(窄容器)也可以放在主内容区(宽容器)。当组件内部有图片时,我们希望它在宽容器下是横排,在窄容器下是竖排。这个需求用容器查询加:has()可以完美实现:

.news-card {
    container-type: inline-size;
}

.news-card:has(img) .inner {
    display: block;
}

@container (min-width: 400px) {
    .news-card:has(img) .inner {
        display: flex;
    }
}

没有图片的卡片始终是竖排,有图片的卡片在容器宽度超过400px时变成横排。这种结合让组件真正做到了“内容感知”和“容器感知”,在项目里可以极大减少模板里的条件表达式。

七、注意性能,别把:has()当万能药

:has()理论上确实比以前的模拟写法慢一点点,尤其是嵌套使用或者在选择器非常复杂的时候。我在实际项目里测试过,现代浏览器对它做了非常多的优化,绝大多数场景下你根本感知不到性能问题。但如果你在一个页面里有几千个卡片,然后写了.card:has(.img):has(.body)这种花哨选择器,那还是会有一些计算成本的。

最好的做法是,尽量让:has()的选择器右边保持简单,比如直接用类名或者标签名,不要在里面再套一个:has()。同时避免在全局重置样式中使用它。

另外,兼容性方面,截至2025年,几乎所有的现代浏览器都支持了:has(),包括Chrome, Edge, Firefox, Safari。如果你还在维护IE时代的旧系统,那就只能老老实实加class了。但我觉得大家可以不必迁就那种老古董项目。

八、:has()还有一个常见误解

有些人认为:has()可以像选择器一样使用正则或者文本内容匹配,其实不能。它只能选择元素的结构关系,不能根据文本内容或数值来筛选。比如你还不能写li:has(text("99"))之类的。所以它只是一个结构上的选择利器,不能干更多魔法。

另外,:has()不能用在所有伪类里,比如你不能在::before里写:has(),它只能作为真正的选择器条件。

九、复盘:我第一次重构后代码变短了多少

我之前做过一个商品规格选择的页面。用户的交互是点击某个色块,然后对应的尺寸列表会变灰。原来代码里要维护一个选中的状态变量,然后根据它给列表容器加上一个class。用了:has()之后,我只需要让选中的radio和它后面的兄弟元素产生关联,然后通过.options:has(input:checked:disabled)来改变颜色。代码量直接砍了一半。

这种感觉就像是我本来需要用JS去“告诉”CSS发生了什么,现在CSS自己能看见了。写前端的时间大幅度转移到样式本身,而不是逻辑的搬运工。

十、写在最后

:has()不是第一个让CSS变得“有状态”的功能,但它是很关键的一个。它让我们可以在纯样式层描述出复杂的条件分支,把交互细节留在HTML和CSS中。以前我们常说HTML负责结构,CSS负责样式,JS负责行为。但现在这个界限被打破了,CSS也开始能决定“因为内部有什么,所以外面长什么样”。

当然,我不建议你用:has()去做所有事。复杂的业务状态依然需要JS和框架来管理。但是在那些“简单结构对应简单视觉”的场景里,能少写几行JS就少写几行,它会让你的代码更简洁,更少bug。毕竟,谁会觉得代码变少不是一件好事呢?

当CSS学会向上看::has() 伪类让表单反馈和卡片布局变得像变魔术
收藏 (0) 打赏

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

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

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

淘吗网 css 当CSS学会向上看::has() 伪类让表单反馈和卡片布局变得像变魔术 https://www.taomawang.com/web/css/2502.html

常见问题

相关文章

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

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