以前做这种效果——鼠标放到某个卡片上,其他兄弟卡片全部变淡,就那张卡片保持原样——第一反应就是上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。这两条互相配合,像锁和钥匙一样缺一不可。
