你有没有想过,网页上那些看似简单的交互效果,背后可能需要多复杂的代码?
比如,你把鼠标移到一个按钮上,整个卡片的样式都变了。或者,你选中了一个选项,整个列表的颜色都跟着改变。这些效果,我们通常需要用 JavaScript 来实现,在 React 这样的框架里,可能就要写不少 state 管理的代码。
前端开发似乎越来越复杂了。我们为了实现一些界面效果,不得不引入庞大的框架和工具链。但有时候,我忍不住会想,有没有更简单的方法?
最近,我发现 CSS 有了一个新的功能,叫做 :has 选择器。它非常强大,强大到可以让我们用一行 CSS 代码,就取代掉原来需要几十行 JavaScript 才能实现的功能。今天,我就想和大家聊聊这个神奇的 :has 选择器。
:has 选择器?学习 CSS 的第一天,我们就知道,它的选择器是从上到下,从外到内的。比如,div p 会选中 div 元素里面的所有 p 元素。我们从来没法“反过来”,让子元素去影响父元素。
这就好比一条单行道,你只能往前开,不能掉头。想让一个段落 p 根据它内部是否有图片 img 来改变自己的样式,在过去是办不到的,只能求助于 JavaScript。
但是,:has 选择器的出现,彻底改变了这一点。它就像给 CSS 开了一扇“后门”,让我们可以选择一个元素的“父元素”或者“前一个兄弟元素”。
举个例子,我们想让所有包含图片的卡片(.card)都有一个特殊的边框,可以这么写:
.card:has(img) {
border: 2px solid blue;
}这行代码的意思是:如果一个 .card 元素“拥有”(has)一个 img 子元素,那么就给这个 .card 元素加上蓝色的边框。是不是非常直观?
这个小小的改变,却为我们打开了一个全新的世界。很多以前必须用 JavaScript 解决的交互问题,现在只用 CSS 就能轻松搞定。
我们来看一个实际的例子。假设我们正在做一个任务看板,上面有很多卡片,每个卡片上都有“打开”和“删除”两个按钮。
为了让键盘用户也能方便地操作,我们希望当用户通过 Tab 键选中某个按钮时,这张卡片能“弹出来”一点,并且根据选中的是“删除”还是“打开”按钮,显示不同的颜色边框。同时,其他未被选中的卡片会变灰,突出显示当前卡片。
如果用 React 来实现,思路大概是这样的:
state,记录当前哪个卡片的哪个按钮被选中了。onFocus 和 onBlur 事件来更新这个 state。state 改变时,父组件会重新渲染,给所有卡片传递新的 props,告诉它们应该显示什么样式。可以想象,为了这么一个效果,代码会变得很复杂,而且每次按 Tab 键,都可能导致所有卡片重新渲染,造成性能问题。这也许就是为什么,我们很少在实际产品中看到这么精细的交互效果。
但是有了 :has 选择器,一切都变得简单了。
首先,我们给按钮加上 data- 属性,方便选中:
<button data-action="open">打开</button>
<button data-action="delete">删除</button>然后,我们用 :has 来改变包含“已选中”按钮的卡片的样式:
/* 选中“删除”按钮时,卡片边框变红 */
.card:has([data-action='delete']:focus-visible) {
transform: scale(1.02);
border-top: 5px solid #f7bccb;
}
/* 选中“打开”按钮时,卡片边框变绿 */
.card:has([data-action='open']:focus-visible) {
transform: scale(1.02);
border-top: 5px solid #c3dccf;
}:focus-visible 是一个伪类,它只在用户通过键盘(比如 Tab 键)获得焦点时才生效,鼠标点击则不会,非常适合做无障碍优化。
接下来,是最神奇的部分,如何让其他卡片变灰?我们同样可以用 :has 来选中“不包含”已选中按钮的卡片。
/* 当任意一个卡片被选中时,让其他所有卡片变灰 */
.cards-container:has(.card:focus-within) .card:not(:focus-within) {
filter: grayscale(80%);
opacity: 0.8;
}就这样,我们没有写一行 JavaScript,就实现了一个非常优雅且高性能的键盘交互效果。代码不仅更简单,而且因为是浏览器原生支持的 CSS,性能也比 React 的方案好得多。

:has 选择器在表单中的应用也非常广泛。
比如,我们希望在一个输入框被禁用(disabled)时,它旁边的标签(label)和描述文字也一起变灰。在以前,这需要 JavaScript 监听状态变化,然后手动给标签添加或移除一个类名。
现在,我们可以这样做:
/* 如果 fieldset 内部有一个被禁用的 input */
fieldset:has(input:disabled) label,
fieldset:has(input:disabled) .description {
color: #d6d6d6;
}同样,当一个列表项里的复选框被选中(:checked)时,我们可以轻松地改变整行的背景颜色:
li:has(input:checked) {
background: #e8f0fe;
}
这些在过去需要用 JavaScript 脚本和状态管理才能实现的效果,现在都变成了纯粹的 CSS 声明。代码的可读性和可维护性都大大提高了。
:has 选择器的出现,让我重新思考了前端开发中 CSS 和 JavaScript 的边界。
我们似乎已经习惯了用 JavaScript 来处理一切与“状态”和“交互”相关的事情。但实际上,很多所谓的“状态”,只是 DOM 元素自身的状态(比如 :focus, :checked, :disabled),它们完全可以在 CSS 内部被消化掉。
过度依赖 JavaScript,不仅让我们的代码变得更复杂,也可能带来不必要的性能开销。有时候,返璞归真,用最简单、最直接的方式去解决问题,反而效果更好。
当然,这并不是说 CSS 可以完全取代 React 或其他框架。React 在管理复杂应用状态、组件化开发等方面依然有巨大优势。但是,对于那些纯粹的、局部的 UI 交互,我们或许可以更多地求助于现代 CSS 的能力。
下一次,当你准备写一个 useState 来控制某个元素的样式时,不妨先停下来想一想:这个问题,能不能只用 CSS 来解决?