鼠标滑过格子间隙就失效,咋用纯CSS搞定兄弟元素淡出?

2,569字
11–16 分钟
in

以前做这种效果——鼠标放到某个卡片上,其他兄弟卡片全部变淡,就那张卡片保持原样——第一反应就是上JS。监听mouseenter和mouseleave,加个类名切来切去。代码写出来倒是能跑,但总觉得不够优雅。直到后来发现CSS里藏着一套纯样式方案,连网格布局中间那条缝隙都不怕了。

目录

核心概念

pointer-events 这属性就像给元素装了个“穿透开关”。设成none的时候,鼠标事件直接穿过该元素,就跟摸空气似的。反过来auto就是正常接收点击和悬停。:hover 伪类负责捕捉鼠标停靠状态,:not(:hover) 则精准排除当前悬停的目标,选中所有没被指到的兄弟。把这几个家伙组合起来,就能实现“容器内除当前悬停项外全部淡出”的交互。

基础版

先搭个最简单的网格,里面放几个方块。假设没有缝隙(gap为0),或者缝隙小到可以忽略,那直接上两行选择器就完事。

<ul class="grid">
  <li class="card"></li>
  <li class="card"></li>
  <li class="card"></li>
</ul>
.grid {
  display: grid;
  grid-template-columns: repeat(3, 120px);
  gap: 0;  /* 没缝隙,或者缝隙极小 */
}

.card {
  background: rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  aspect-ratio: 1 / 1;
  transition: opacity 0.2s;
}

/* 当鼠标悬浮在网格容器上时,所有卡片透明度降低 */
.grid:hover .card {
  opacity: 0.4;
}

/* 但是当前被悬浮的那张卡片,要把透明度拉回来 */
.grid:hover .card:not(:hover) {
  opacity: 0.4;  /* 这里写重复了?看下面解释 */
}

等等,上面那块写错了。正确逻辑是:容器悬浮时让所有卡片变淡,但排除掉当前悬停的那张。所以应该是:

.grid:hover .card {
  opacity: 0.4;
}

.grid:hover .card:hover {
  opacity: 1;
}

或者更简洁地用:not

.grid:hover .card:not(:hover) {
  opacity: 0.4;
}

这样鼠标一进网格,没被指到的卡片全变淡。如果网格内元素之间没有空隙,鼠标在卡片之间移动非常丝滑。但一旦gap设成1rem,问题就来了——当鼠标从卡片A滑向卡片B,中间那段空白区域会触发网格容器的悬浮,导致所有卡片瞬间变成半透明,体验直接崩掉。

间隙克星

解决这个翻车现场只需要两行额外代码。核心思路:让网格容器“看不见”鼠标事件,但里面的每个孩子正常接收。这样鼠标就算停在缝隙上,容器也不会响应:hover,只有真正落到卡片上才会触发效果。

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, 15rem);
  gap: 1.5rem;
  pointer-events: none;  /* 第一行:容器变透明盾牌 */
}

.card {
  background: rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  aspect-ratio: 1 / 1;
  pointer-events: auto;   /* 第二行:卡片恢复接收事件 */
  transition: opacity 0.2s;
}

/* 悬浮时照样让非悬浮卡片淡出 */
.grid:hover .card:not(:hover) {
  opacity: 0.3;
}

实际操作时,先写好基础样式和gap,然后把pointer-events: none甩给.grid。这时候如果直接刷新页面,会发现所有卡片都点不动了,因为事件被容器吃掉。紧接着给.card加上pointer-events: auto,卡片立刻恢复可交互状态。鼠标现在划过网格缝隙,什么都不会发生;一旦落进任意卡片,其他兄弟马上变淡。这个方案对二维网格同样适用,哪怕有十几张卡片排成两行三列,缝隙再宽也不怕。

有一点得留神:pointer-events: none会让整个容器变成“事件黑洞”,如果网格外面还包了其他需要监听的父级,可能会影响全局。另外,如果网格本身需要响应点击事件(比如整块区域绑了点击跳转),那这个方案就不太合适,因为容器压根收不到任何鼠标行为。

滚动翻车

这种套路碰到滚动容器会出岔子。比如做一个横向滑动的卡片列表,外层设overflow-x: auto,内部网格带pointer-events: none。后果就是滚动条拖不动了,因为所有鼠标事件都被忽略。这时候可以给.grid外面再裹一层普通div,滚动操作交给这个外层,内部的pointer-events技巧照用不误。

<div class="scroll-wrapper">
  <ul class="grid">
    <li class="card"></li>
    <li class="card"></li>
    <!-- 更多卡片... -->
  </ul>
</div>
.scroll-wrapper {
  overflow-x: auto;
  /* 不加 pointer-events,保持正常滚动 */
}

.grid {
  display: flex;  /* 或者用 grid,但保证内容能撑开 */
  gap: 1rem;
  pointer-events: none;
}

.card {
  flex-shrink: 0;
  width: 200px;
  height: 200px;
  pointer-events: auto;
}

滚动区域由外层div接管,网格内部继续用穿透大法。不过这样一来,鼠标从空白间隙划过时,滚动条依然可以拖拽,但卡片淡出效果只在真正落进卡片时才触发。实测中如果外层滚动条和内部卡片距离太近,可能会有一瞬间的误触发,但只要gap不是特别大,体感影响很小。

方案优点局限
基础版代码极少有gap就翻车
间隙克星无视任何缝隙容器不能滚动
嵌套滚动保留滚动能力多一层包裹

动手练的时候,建议先用三四个卡片在本地试。打开浏览器开发者工具,勾选:hover状态观察不同区域的触发情况。一旦发现鼠标在缝隙间乱晃时所有卡片忽明忽暗,那八成是忘记给容器加pointer-events: none。反过来如果卡片完全点不动,那就是孩子没恢复auto。这两条互相配合,像锁和钥匙一样缺一不可。