当光标靠近元素时,元素会产生一种"被吸引"的微妙偏移;鼠标在元素上移动时,卡片会像真实物体一样沿 X/Y 轴倾斜。两种效果叠加,让 UI 拥有物理质感和生命力。
效果预览
一、效果拆解
这个交互由两层独立的物理效果叠加而成,理解它们的数学原理是写好代码的前提。
1.1 磁性吸附(Magnetic Pull)
现象:光标靠近元素时,元素会微微"迎向"光标移动;离开时缓慢复位。
数学原理:以元素中心为原点,计算光标到原点的距离 dist:
power = 1 - dist / magnetRadius (dist < magnetRadius 时有效)
offsetX = distX * power * (magnetIntensity / magnetRadius)
offsetY = distY * power * (magnetIntensity / magnetRadius)power 是一个 0~1 的衰减系数——光标越近,系数越大,偏移量越大。离开触发半径后用 lerp 平滑归零。
用途场景:
- 导航菜单项:鼠标靠近时微微放大并上移
- CTA 按钮:光标靠近时产生"吸附感",引导点击
- 图片画廊:鼠标靠近缩略图时产生微妙的聚拢效果
1.2 3D 倾斜(Tilt Effect)
现象:鼠标在元素上移动时,元素沿 X 轴和 Y 轴旋转,产生真实的透视立体感。
数学原理:同样以元素中心为原点,计算光标相对位移的归一化值:
tiltX = (mouseY - centerY) / (height / 2) * tiltIntensity
tiltY = (mouseX - centerX) / (width / 2) * tiltIntensity配合 CSS perspective 和 rotateX/rotateY,实现三维空间旋转。值域钳制在 -tiltIntensity ~ +tiltIntensity之间,防止倾斜过度。
用途场景:
- 商品卡片:鼠标扫过时展示多角度立体感
- 头像框:创造轻微的悬浮感
- 数据仪表卡:让数字面板更有质感
二、完整代码
HTML 结构
<div class="magnetic-wrapper">
<div class="magnetic-card" id="magneticCard">
<!-- 动态光泽层 -->
<div class="card-glare" id="cardGlare"></div>
<span class="card-tag">CSS Effect</span>
<h2 class="card-title">Magnetic Cursor & 3D Tilt</h2>
<p class="card-subtitle">光标磁性吸附与三维倾斜叠加效果……</p>
<div class="card-divider"></div>
<div class="card-meta">
<div class="card-avatar">FL</div>
<div class="card-author-info">
<div class="card-author">Flora</div>
<div class="card-date">2026 · 03 · 19</div>
</div>
</div>
<div class="card-tags">
<span class="card-tag-item">JavaScript</span>
<span class="card-tag-item">CSS 3D</span>
</div>
<div class="card-footer">
<span class="card-stat"><strong>∞</strong> reads</span>
<span class="card-btn">Read More →</span>
</div>
</div>
</div>核心 CSS
.magnetic-card {
/* 保留 3D 空间,否则 rotateX/Y 不生效 */
transform-style: preserve-3d;
/* 告诉浏览器提前检测变换,用于 GPU 加速 */
will-change: transform;
/* 初始状态平滑过渡 */
transition:
transform 0.08s ease-out,
box-shadow 0.3s ease;
}
/* 动态光泽:跟随光标位置实时更新背景 */
.card-glare {
background: radial-gradient(
circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(255, 255, 255, 0.12) 0%,
transparent 60%
);
opacity: 0;
transition: opacity 0.3s ease;
}
.magnetic-card:hover .card-glare {
opacity: 1; /* 悬停时才显示光泽 */
}
/* 动态阴影:跟随倾斜方向偏移 */
.magnetic-card {
box-shadow:
/* 基础阴影 */
20px 20px 60px rgba(0, 0, 0, 0.5),
/* 彩色氛围光(随偏移量动态变化) */
var(--shadow-x, 0) var(--shadow-y, 0) 20px rgba(99, 102, 241, 0.15),
/* 边框 */
0 0 0 1px rgba(255, 255, 255, 0.05);
}JavaScript 逻辑
const CONFIG = {
tiltIntensity: 15, // 最大倾斜角度(度)
magnetIntensity: 40, // 磁性吸附最大偏移量(px)
magnetRadius: 120, // 触发磁性吸附的光标距离(px)
resetSpeed: 0.08, // 复位速度(越小越顺滑)
tiltSpeed: 0.15, // 倾斜跟随速度
};
let currentTiltX = 0, currentTiltY = 0;
let targetTiltX = 0, targetTiltY = 0;
let magnetOffsetX = 0, magnetOffsetY = 0;
function lerp(start, end, factor) {
return start + (end - start) * factor;
}
// 每帧更新卡片状态
function updateCard() {
// 倾斜:快速跟随鼠标
currentTiltX = lerp(currentTiltX, targetTiltX, CONFIG.tiltSpeed);
currentTiltY = lerp(currentTiltY, targetTiltY, CONFIG.tiltSpeed);
// 磁性偏移:鼠标离开后缓慢归零
magnetOffsetX = lerp(magnetOffsetX, 0, CONFIG.resetSpeed);
magnetOffsetY = lerp(magnetOffsetY, 0, CONFIG.resetSpeed);
// 复位完成后停止动画
if (Math.abs(targetTiltX) < 0.01 && Math.abs(targetTiltY) < 0.01 &&
Math.abs(magnetOffsetX) < 0.01 && Math.abs(magnetOffsetY) < 0.01) {
return; // 动画停止,下次 mousemove 重新启动
}
card.style.transform =
`translate(${magnetOffsetX}px, ${magnetOffsetY}px) ` +
`perspective(1000px) ` +
`rotateX(${-currentTiltY}deg) ` +
`rotateY(${currentTiltX}deg)`;
requestAnimationFrame(updateCard);
}
// 鼠标移动:计算两个效果的目标值
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// --- 3D 倾斜 ---
const dx = (e.clientX - centerX) / (rect.width / 2);
const dy = (e.clientY - centerY) / (rect.height / 2);
targetTiltX = dy * CONFIG.tiltIntensity; // 上下 → rotateX
targetTiltY = dx * CONFIG.tiltIntensity; // 左右 → rotateY
// --- 磁性吸附 ---
const distX = e.clientX - centerX;
const distY = e.clientY - centerY;
const dist = Math.hypot(distX, distY); // = √(dx² + dy²)
if (dist < CONFIG.magnetRadius) {
const power = 1 - dist / CONFIG.magnetRadius;
magnetOffsetX = distX * power * (CONFIG.magnetIntensity / CONFIG.magnetRadius);
magnetOffsetY = distY * power * (CONFIG.magnetIntensity / CONFIG.magnetRadius);
}
// --- 更新光泽层位置 ---
const pctX = ((e.clientX - rect.left) / rect.width) * 100;
const pctY = ((e.clientY - rect.top) / rect.height) * 100;
glare.style.setProperty('--mouse-x', pctX + '%');
glare.style.setProperty('--mouse-y', pctY + '%');
});
// 鼠标离开:复位
card.addEventListener('mouseleave', () => {
targetTiltX = targetTiltY = 0;
magnetOffsetX = magnetOffsetY = 0;
});
// 鼠标进入:启动动画循环
card.addEventListener('mouseenter', () => {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(updateCard);
});三、参数调优指南
| 参数 | 默认值 | 调大效果 | 调小效果 |
|---|---|---|---|
| tiltIntensity | 15° | 倾斜更夸张,3D 感更强 | 轻微倾斜,更克制 |
| magnetIntensity | 40px | 吸附感更强,更"黏" | 几乎无吸附感 |
| magnetRadius | 120px | 更大范围触发吸附 | 仅在卡片附近触发 |
| resetSpeed | 0.08 | 快速弹回,干脆利落 | 缓慢归位,丝滑感 |
| tiltSpeed | 0.15 | 跟得更紧,响应更快 | 跟随滞后,更柔和 |
实际调优建议:
- 商品展示卡片:tiltIntensity: 20~25,偏夸张,增强吸引力
- 正式工具界面:tiltIntensity: 8~12,resetSpeed: 0.12,克制冷静
- 移动端触控:把 mousemove 换成 touchmove,倾斜 Intensity 减半
四、性能优化要点
4.1 will-change 的正确使用
.magnetic-card {
/* 在动画即将开始前添加,开始后保持,结束后移除 */
will-change: transform;
}不要在 CSS 中永久设置 will-change,只有在需要时才声明,否则会浪费 GPU 内存。
4.2 requestAnimationFrame 替代 setInterval
60fps 的定时更新逻辑用 requestAnimationFrame,它会在浏览器下一次重绘前执行,避免丢帧和卡顿。
4.3 限制计算频率
如果页面上有多张可交互卡片,可以对 mousemove 事件做节流(throttle),每 16ms(1帧)最多计算一次:
let lastTime = 0;
card.addEventListener('mousemove', (e) => {
const now = performance.now();
if (now - lastTime < 16) return;
lastTime = now;
// ...执行计算逻辑
});五、扩展思路
进阶 1:多元素联动
在磁性吸附范围内同时影响其他元素(比如周围的图标也向光标靠拢),形成"涟漪"效果。
进阶 2:倾斜引导注意力
当卡片倾斜时,将高光区域(光泽层)引导到你想让用户注意的 CTA 按钮上,利用倾斜自然引导视线。
进阶 3:3D 翻转
在 mousemove 中检测移动方向(从左向右 vs 从右向左),配合 rotateY,可以做到让元素"转身"的微妙效果。
原文链接:荣小站 xgr.cab
代码仓库:所有代码可直接保存为 .html 文件在浏览器中运行预览

评语 (0)