【每天一个CSS | Day08】整排按钮被鼠标吸走的磁吸导航

写在前面

视觉系周昨天收官,从今天起进入交互系周。

前七天做的都是"看着好看"的效果------光、像素、立体、插画。它们有一个共同点:你不碰它,它就在那里自顾自地演。

从今天开始,我们要做"会回应你"的效果。

第一篇是最经典的交互感之一:磁吸导航。

鼠标悬停任意一颗按钮,它带3D倾角弹起、点亮;其余四颗一起缩小、降饱和"让位"------整条导航像被鼠标"吸"住。

听起来需要实时鼠标坐标计算,是JS的活儿。但今天要用纯CSS 做出90%的观感。

核心只有一条兄弟选择器 + 一组CSS变量。


一、先看效果

深色房间里一条胶囊导航,五颗按钮:

  • 鼠标扫过时:被指到的按钮带3D倾角向上弹起、背景变亮、文字变白、底部多一道光晕
  • 其余四颗:一起缩小、降饱和度、变暗------像往后退了一步
  • 按钮的倾角是按位置预设的:最左边那颗往右倾14°,最右边那颗往左倾14°,中间那颗只往前压

视觉上就像每颗按钮都被"吸"向指针所在的位置,鼠标还没碰到,整排已经在动。

怎么跑起来:

  1. 新建一个demo.html
  2. 把下面第二节的完整代码粘进去
  3. 双击用浏览器打开

📸 截图党提示 :效果最佳时刻是鼠标悬停在第二颗按钮 上------一颗凸起、四颗让位的瞬间。建议按F11 全屏后截图;或者录4秒GIF,让鼠标从左到右缓缓扫过整排,观感更完整。


二、完整代码(复制即用)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Day 08 · 磁吸导航:整排按钮被鼠标吸走</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { height:100%; }
  body { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:60px;
    overflow:hidden; background:#0d1117; font-family:"PingFang SC", sans-serif; }

  .head { text-align:center; color:#e6edf3; }
  .head h1 { font-size:22px; letter-spacing:.3em; font-weight:600; }
  .head p { margin-top:10px; font:400 11px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.3em; color:#5b6878; }

  /* ------ 磁吸导航:容器给透视,按钮各自带预设倾角 ------ */
  .nav { display:flex; gap:8px; padding:12px; border-radius:20px;
    background:#141a22; border:1px solid rgba(255,255,255,.06);
    perspective:600px; }

  .btn { display:flex; align-items:center; gap:10px; padding:16px 22px;
    border:1px solid rgba(255,255,255,.05); border-radius:14px; cursor:pointer;
    background:#1b232e; color:#9fb0c3; font-size:13px; letter-spacing:.14em;
    transition: transform .28s cubic-bezier(.34,1.56,.64,1), background .28s, color .28s,
                box-shadow .28s, opacity .28s, filter .28s; }
  .btn i { width:8px; height:8px; border-radius:50%; background:var(--c,#7ab8ff); }
  .btn:hover { transform: perspective(400px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg))
                 translateY(-8px) scale(1.12);
    background:#243044; color:#fff;
    box-shadow: 0 18px 30px rgba(0,0,0,.5), 0 0 24px rgba(122,184,255,.12); }

  /* ------ 磁吸感的核心:hover 一颗,其余整体「让位」------ */
  .nav:hover .btn:not(:hover) { transform:scale(.88); opacity:.55;
    filter:saturate(.5) brightness(.8); }

  .b1 { --c:#7fe3d2; --ry:14deg;  --rx:-4deg; }
  .b2 { --c:#b9a7ff; --ry:7deg;   --rx:-9deg; }
  .b3 { --c:#ffc2cf; --ry:0deg;   --rx:-13deg; }
  .b4 { --c:#ffd479; --ry:-7deg;  --rx:-9deg; }
  .b5 { --c:#7ab8ff; --ry:-14deg; --rx:-4deg; }

  .hint { font:400 10px/1 "SF Mono", Consolas, monospace; letter-spacing:.3em; color:#3d4a5a; }
  .tag { position:fixed; right:18px; bottom:14px; font:400 10px/1 "SF Mono", Consolas, monospace;
    letter-spacing:.28em; color:rgba(159,176,195,.3); }
</style>
</head>
<body>
  <div class="head">
    <h1>MAGNETIC NAV</h1>
    <p>HOVER A BUTTON · THE ROW FOLLOWS</p>
  </div>
  <div class="nav">
    <button class="btn b1"><i></i>HOME</button>
    <button class="btn b2"><i></i>WORK</button>
    <button class="btn b3"><i></i>LAB</button>
    <button class="btn b4"><i></i>BLOG</button>
    <button class="btn b5"><i></i>INFO</button>
  </div>
  <div class="hint">ZERO JS · PURE CSS MAGNETISM</div>
  <div class="tag">每天一个惊艳 CSS · DAY 08</div>
</body>
</html>

三、三个核心技法拆解

代码不长,但三个点层层递进------每一个单独拿出来都能用在别的地方。

技法1:让位------一条兄弟选择器制造「整体被吸走」

磁吸感的真正来源,不是hover的那颗按钮,而是其余按钮的集体反应。

如果只有被 hover 的那颗弹起,其余四颗纹丝不动,观感是"普通按钮hover效果"。真正让它变成"磁吸"的,是这一行:

css 复制代码
.nav:hover .btn:not(:hover) {
  transform: scale(.88);
  opacity: .55;
  filter: saturate(.5) brightness(.8);
}

逐层读一遍:

部分 意思
.nav:hover 鼠标在这条导航的任意位置(包括按钮之间的间隙)
.btn:not(:hover) 其中没被指到的按钮
合起来 "容器被hover,但自己不是主角"的所有按钮

为什么要加.nav:hover这一层?

这是这条选择器唯一容易写错的地方:

css 复制代码
/* ❌ 少了容器约束:所有没被 hover 的按钮永远缩小 */
.btn:not(:hover) { transform: scale(.88); }

/* ✅ 有容器约束:只有"导航被 hover 时"才触发让位 */
.nav:hover .btn:not(:hover) { transform: scale(.88); }

少了.nav:hover前缀,页面一打开,所有没被鼠标指到的按钮就会全部缩小。观感立刻毁掉------你想要的"整排被吸走",变成了"页面默认就是一堆缩小的按钮"。

这条选择器是纯CSS交互的通用利器:

  • 画廊hover:悬停一张,其余半透明
  • 卡片墙:悬停一张,其余往后退
  • 标签云:悬停一枚,其余变淡
  • Tab 栏:悬停一个,其余降级

一个模式,套到哪里都成立。


技法2:按位置预设倾角,模拟「朝鼠标倾斜」

真正的"朝鼠标方向倾斜"需要实时获取鼠标坐标,那是JS的活儿。

但我们退一步想:按钮在整排里的位置是固定的。

  • 最左边的按钮,鼠标肯定从右边靠近它
  • 最右边的按钮,鼠标肯定从左边靠近它
  • 中间的按钮,鼠标从正前方靠近它

所以给每颗按钮预设一组倾角就够了:

css 复制代码
.b1 { --ry:14deg;  --rx:-4deg; }   /* 最左:向右倾,微微前压 */
.b2 { --ry:7deg;   --rx:-9deg; }   /* 次左:向右倾 */
.b3 { --ry:0deg;   --rx:-13deg; }  /* 中间:只前压,不左右倾 */
.b4 { --ry:-7deg;  --rx:-9deg; }   /* 次右:向左倾 */
.b5 { --ry:-14deg; --rx:-4deg; }   /* 最右:向左倾 */

hover 时统一启用:

css 复制代码
.btn:hover {
  transform: perspective(400px)
             rotateX(var(--rx, 0deg))
             rotateY(var(--ry, 0deg))
             translateY(-8px)
             scale(1.12);
}

为什么用CSS变量?

因为同样的transform公式,不同的按钮用不同的数值。

如果不用变量,每颗按钮都要写一遍完整的transform------5颗按钮就是5份重复代码,改一个参数要改5处。

用变量之后:

  • 公式写一次 (.btn:hover里)
  • 数值分别给 (.b1 ~ .b5里)
  • 改一颗只改一行

这是CSS变量最典型的用法:把"公式"和"参数"解耦。

观感验证 :把鼠标从左往右缓缓扫过,你会发现每颗按钮都像"朝着鼠标的方向微微转了一下"。和真·实时计算几乎没差,成本为零。


技法 3:弹性缓动 = 磁铁的「回弹」

transition的缓动函数用这条:

css 复制代码
transition: transform .28s cubic-bezier(.34, 1.56, .64, 1),
            background .28s, color .28s, box-shadow .28s, opacity .28s, filter .28s;

重点是那串cubic-bezier(.34, 1.56, .64, 1)。

cubic-bezier(x1, y1, x2, y2)里的第二个参数y1 = 1.56超过了1------这代表"动画会冲过目标位置再弹回来"。

普通ease曲线只会平滑地滑到终点,像橡皮筋慢慢收紧;过冲曲线会先冲过头、再回弹,像真的被吸铁吸过去、碰一下、顿住。

磁吸的"吸"劲儿,一半来自位移动画,另一半就来自这个过冲。

为什么是.28s?

  • 太短(< 0.15s)→ 感觉像"啪"地闪现,没有"吸"的过程
  • 太长(> 0.5s)→ 鼠标已经移开了按钮还在慢慢弹,反馈迟钝

.28s是"有过程但不拖沓"的甜点位,和人手移动的自然节奏契合。

注意:过冲缓动不能用在位移过大的场景。

css 复制代码
/* ❌ 位移太大 + 过冲 = 弹得很夸张、像故障 */
transform: translateY(-30px) scale(1.5);

/* ✅ 磁吸的位移控制在 10px 内最自然 */
transform: translateY(-8px) scale(1.12);

位移越大,过冲幅度按比例放大。translateY(-8px)配过冲正好;换成-30px就会弹出"果冻感"。


四、踩坑位(我全踩过,你直接避)

① :not(:hover)忘了限定在容器hover之下

css 复制代码
/* ❌ 页面一打开,所有没被指到的按钮全部缩小 */
.btn:not(:hover) { transform: scale(.88); }

/* ✅ 必须写全「容器 hover + 自己不是主角」 */
.nav:hover .btn:not(:hover) { transform: scale(.88); }

**这是这条选择器唯一容易写错的地方。**少了.nav:hover前缀,效果就从"整排被吸走"变成"默认就是一堆缩小的按钮"。

记住口诀 :先声明"场景"(容器被hover),再声明"角色"(自己不是主角)。

② perspective放在按钮自己身上

css 复制代码
/* ❌ 每颗按钮自转自看,5 颗各自透视、没有统一灭点 */
.btn:hover { transform: perspective(400px) rotateY(...); }

/* ✅ 透视给容器,按钮只写旋转 */
.nav { perspective: 600px; }
.btn:hover { transform: rotateX(...) rotateY(...) translateY(-8px) scale(1.12); }

perspective写在容器上,所有子元素共享同一个"消失点"------就像整个场景在镜头前,而不是每颗按钮自己拿一台相机。

如果写在按钮上 ,每颗按钮的透视都是"自己相对自己"的------左边那颗和右边那颗的倾角看起来一样,磁吸的"朝不同方向倾"就消失了。

但要注意 :我这里在.btn:hover里还写了一次perspective(400px)。这是"内联透视"写法------为了和rotateX/rotateY一起组成一个独立的3D上下文,效果更可控。两种写法都能用,但不要混 :容器给perspective是一种风格,元素内部perspective()函数是另一种风格。

③ transition只写transform

css 复制代码
/* ❌ 只有位移是平滑的,颜色和阴影突变 */
.btn { transition: transform .28s; }

/* ✅ 所有参与变化的属性都列进 transition */
.btn {
  transition: transform .28s cubic-bezier(.34,1.56,.64,1),
              background .28s, color .28s, box-shadow .28s,
              opacity .28s, filter .28s;
}

磁吸按钮hover时同时变了6个属性:transform、background、color、box-shadow、opacity、filter。

只有transform走transition,其余五样瞬间切换------瞬间切换的属性和平滑过渡的属性混在一起,"吸"的错觉就露馅了:按钮是飘上去了,但背景"啪"地一闪,很出戏。

通用心法 :*一个元素hover时改了几个属性,transition里就写几个属性。*宁可多写,不可漏写。

④ 过冲缓动用在位移过大的场景

css 复制代码
/* ❌ 位移-30px + 过冲曲线 = 弹得很夸张、像果冻 */
transform: translateY(-30px) scale(1.4);

/* ✅ 位移控制在10px内 */
transform: translateY(-8px) scale(1.12);

过冲幅度和位移幅度成正比。位移越大,回弹越夸张。磁吸这种需要"克制高级感"的效果,位移10px以内最自然。

如果确实需要大位移,就把过冲缓动换成普通ease-out,或者把cubic-bezier的第二个参数调到1.2左右(轻微过冲)。


五、变式挑战(10--30分钟,动手才有收获)

任务:升级成真·逐像素磁吸。

现在这版是"预设倾角"------每颗按钮的倾角是写死的。

进阶版 :让鼠标在按钮内部移动 时,倾角实时变化。指针移到左上角,按钮往左上倾;移到右下角,往右下倾。

需要 +6行JS(本专栏第一次真正的"半开戒"):

js 复制代码
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('pointermove', e => {
    const r = btn.getBoundingClientRect();
    btn.style.setProperty('--ry', ((e.clientX - r.left) / r.width  - .5) * 24 + 'deg');
    btn.style.setProperty('--rx', ((.5 - (e.clientY - r.top) / r.height) * 18) + 'deg');
  });
  btn.addEventListener('pointerleave', () => {
    btn.style.setProperty('--ry', '0deg');
    btn.style.setProperty('--rx', '0deg');
  });
});

注意:CSS一行都不用改。

transform里用的还是var(--ry)和var(--rx),只是这两个变量的值从"预设"变成了"实时计算"。

这就是CSS变量做交互接口的好处:

  • CSS负责"怎么用 "(transform公式、transition曲线)
  • JS负责"给什么值"(鼠标坐标换算)
  • 两者解耦,各司其职

做完你会惊讶:加了JS之后,那6行代码竟然这么短------因为CSS已经把90%的活儿干完了。

其他变式方向(选做):

  • 换成卡片墙:把5颗按钮换成5张产品卡,hover时主角放大、其余后退,就是一个磁吸画廊
  • 加"高光扫过" :hover时在被指到的按钮上叠一层斜向白色渐变(::before),做"光扫过去"的质感
  • 反向磁吸 :让非hover 的按钮朝主角方向移动 (translateX),而不是缩小------"吸附"的物理感更直白

六、小结

今天这一篇里藏着 5个通用技能点:

技能 适用场景
.parent:hover .child:not(:hover) 让位模式 画廊、卡片墙、Tab栏、标签云
CSS变量做"公式 + 参数"解耦 所有"同款式、不同数值"的组件
按位置预设参数,模拟实时计算 所有"接近鼠标即响应"的效果
cubic-bezier的y > 1过冲缓动 所有"吸住""弹回""果冻"质感
容器统一perspective 所有多元素3D场景

最值得记住的两条:

① .parent:hover .child:not(:hover)是纯CSS交互的通用骨架。

这条选择器让"某个元素被指到 → 其余元素让位"变得一行可达。它不挑元素类型,从按钮到卡片到标签,套到哪都成立。

② CSS变量不只用来"存颜色",更用来"传参数"。

今天用它存倾角,Day03用它存楼高、色相,Day04用它存月相延迟。它是CSS里唯一能把"公式"和"数值"分开写的工具------学会它,重复代码就能直接砍掉一半。


如果这一篇对你有帮助,三连支持一下这个专栏 🙏

点赞 = 更新加速,收藏 = 随时复制代码,评论 = 你想看什么效果,我排进日程。

本专栏所有demo,复制即用,随便商用。

目标更新:不断更。

相关推荐
就叫年华吧丶1 小时前
长文档点目录定位总是不准?一个 content-visibility + 平滑滚动引发的连环坑(Vue3 实战)
前端·javascript·算法·vue
JudithHuang1 小时前
React 路由:React Router
前端·react.js·前端框架
赵得C1 小时前
创建 SVG 图标预览页面:从零实现到解决 CORS 问题
前端·javascript·html
IT_陈寒1 小时前
Java的HashMap线程安全问题让我深夜掉光了头发
前端·人工智能·后端
三掌柜6661 小时前
ArkWeb 手记 04|Web 返回键别再一刀切
前端·harmonyos
光影少年2 小时前
RN与Flutter架构区别
前端·flutter·react native·react.js·架构·node.js
peter67682 小时前
css学习小结
前端·css·学习
liangshanbo12152 小时前
什么是CSS原子化?它的优劣势是什么?
前端·css·tensorflow·css原子化
.道阻且长.2 小时前
C++ 11:可变参数模板
前端·c++·算法