前几天做可视化大屏重构,遇到一个听起来特别适合“上 JavaScript”的交互:页面左侧是按月份排布的趋势柱状图,右侧是对应月份的明细列表,产品要求鼠标悬停柱状图的某一根柱子时,右侧表格里同月份的整行高亮;反过来,鼠标放进表格某一行时,左侧的对应柱子也要高亮。更麻烦的是,这种模块在页面上不止一个,有折线图配列表、有地图点位配排行,规则几乎一致,都是“多组兄弟元素之间做悬停高亮联动”。如果按老办法一个个注册 mouseenter、逐个维护 class,代码量不小,改起来也容易漏。后来我把整个交互收敛成一套纯 CSS 的自动化方案,没有写一行事件逻辑,靠 SCSS 编译期循环加 CSS :has() 选择器,把所有联动规则批量生成出来。这篇文章就完整记录这套思路、踩坑过程以及可直接抄走的实现模板,适合正在做 Dashboard、数据报表、可视化大屏,或者经常被“列表和图表相互高亮”需求折磨的前端同学。
1. 需求拆解:多组兄弟元素的联动到底在联什么
1.1 我先复述一遍最初的业务场景
页面里有一个“运营趋势总览”模块,布局是左右结构。左边画了六根柱子,每根表示一个月的销售达成率;右边是一张六行的表格,每行放着对应月份的明细,比如目标值、完成值、同比、环比。需求交互很明确:
- 鼠标移到左边第 3 根柱子上,右边表格第 3 行背景变蓝,同时这一行可以稍微把文字加粗,用户视线能瞬间沿横向扫过去;
- 鼠标移到右边第 5 行时,左边第 5 根柱子也要变色,甚至可以“拔高”一点,用一个放大效果让用户看出右侧这条数据在左侧对应哪个点。
这样的联动其实在做一件事:让两个互相独立、但又共享同一维度顺序的区域,在某一维度项被 hover 时,都高亮“当前正在看的这一项”。如果用数据结构的话说,两组兄弟元素之间天然存在一个同序索引映射:第一组第 N 项,对应第二组第 N 项。
页面上一共有三个大模块要这么联,有的模块里是三组对应,有的甚至是四组对应。如果每个模块都写一套 JS,先 querySelectorAll 拿两组节点,再循环绑定事件,接着还要在事件里用 index 去找另一组的兄弟节点,逻辑大同小异,但每个模块都要写一遍,每个都要考虑离开时清样式、连击时会不会闪烁、插入新数据后索引是否还正确。
这也是我思考“自动化方案”的起点:能不能让交互状态根本不去操作 DOM,而是让 CSS 自己知道“当前 hover 的是哪个索引”,然后把这个信息广播给同容器下的所有兄弟分组?
1.2 把问题抽象成“悬停源”和“联动目标”
为了后面写方案的清晰,我给这套机制定义两个角色:
- 悬停源:用户鼠标真正停留在上面的那一组元素;
- 联动目标:因为悬停源进入 hover 状态而需要同步高亮的那一组元素。
比如左边柱状图是悬停源时,右边表格就是联动目标;反过来,右边表格是悬停源,左边柱状图就变成联动目标。实际开发里,一个模块中常常是两组互为悬停源、互为联动目标,也就是双向联动。
这个抽象非常重要,因为它决定了我后续写 SCSS 混入时的参数。悬停源允许有多个,联动目标也允许有多个。举例来说,一个区块顶部是图例,中间是折线,下面是明细表,你希望悬停图例某一项时,折线上的对应点和对应表格行全部高亮,这时候“悬停源”是图例组,“联动目标”是剩下的折线和表格。
把这些角色理清楚后,整个方案要解决的问题就剩下一个:对每一对“悬停源组元素 + 联动目标组元素”,是否有一种纯 CSS 的方式知道当前 hover 的源元素序号,并把序号对应到目标组的同序号元素上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 纯 CSS 可行的三个底层机制,以及为什么 :has() 是关键
2.1 传统兄弟选择器为什么做不到跨区域联动
以前想写纯 CSS 的 hover 联动,最常见的手段是利用相邻兄弟选择器 + 和通用兄弟选择器 ~。例如:
css复制.title:hover + .content {
border-color: red;
}
这个模式要求悬停源和联动目标在结构上是紧密相邻的兄弟。但我实际业务里的柱状图容器和表格容器并不是直接相邻,它们之间隔着布局元素,而且左侧柱子那一组和右侧表格那一组,可能在 DOM 上各自处在不同的深层容器中。如果只用兄弟选择器,你要么把结构强行改成“hover 元素后面直接跟着一个联动目标”,要么只能在同一组内部的兄弟之间做高亮。
还有另一个更反直觉的限制:~ 只能选择目标位于 hover 源之后且在同一个父容器内的元素。如果联动目标在 hover 源前面,那 CSS 常规写法完全没有办法在 hover 源状态下去选中它。元素选择只能单向流动,纯旧 CSS 在跨组、反向、多深度的场景上会马上碰壁。
那真正的解法不是去补“兄弟之间的直接选择”,而是把 hover 状态提升到它们共同的父容器上,让父容器成为状态广播中心,再从这个中心向下派发样式。
2.2 :has() 好比给 CSS 装上了事件委托
2022 年底开始,主流浏览器逐步实现了 :has() 选择器。它的能力通俗说就是:我不仅可以根据某个元素自身状态选中它,还可以根据它的后代或兄弟状态反过来选中它自己。
例如:
css复制.parent:has(.child:hover) {
background: #f5f7fa;
}
意思是:如果某个 .parent 内部存在一个正处于 hover 状态的 .child,那么这个 .parent 就匹配这条规则。这跟我们平时说的 CSS 事件委托非常像。JavaScript 里有个事件委托策略,是让父节点统一监听子节点冒泡上来的事件;CSS 的 :has() 虽然不处理事件,但它把“某个后代
