效果拆解
整个鼠标效果由三层组成,逐层叠加:
| 层级 | 实现方式 | 作用 |
|---|---|---|
| 1 | cursor: url(data:image/svg+xml...) 替换系统光标 | 白色小圆点代替默认箭头,零延迟 |
| 2 | <div id="g-pointer"> 跟随鼠标移动 | 灰色半透明圆,0.1s 过渡延迟产生粘滞感 |
| 3 | .click-effect 动画元素 | 鼠标松开时扩散涟漪,动画结束后移除 |
关键视觉点是第二层的延迟跟随——白色光标在鼠标实际位置,灰色圆因为 transition: transform 0.1s ease会略微滞后,快速移动时两层错开,产生层次感
方案选择
定位跟随元素有两种方式,性能差异明显:
| 方案 | 触发流程 | 性能 |
|---|---|---|
left / top | Layout → Paint → Composite | 差,高频mousemove下卡顿 |
transform: translate() | Composite 仅合成层 | 好,GPU 加速 |
使用 will-change: transform 提前告知浏览器分配独立合成层
实现
CSS
/* SVG 光标:亮色用黑点,暗色用白点 */
html.custom-cursor-active,
html.custom-cursor-active * {
cursor: url("data:image/svg+xml,...fill='black'...") 4 4, auto !important;
}
html.dark.custom-cursor-active,
html.dark.custom-cursor-active * {
cursor: url("data:image/svg+xml,...fill='white'...") 4 4, auto !important;
}
/* 跟随灰圆:transform 定位 + 过渡延迟 */
#g-pointer {
position: fixed;
top: 0;
left: 0;
width: 20px;
height: 20px;
background: rgba(128, 128, 128, 0.3);
border-radius: 50%;
pointer-events: none;
z-index: 9999999;
transition: transform 0.1s ease;
will-change: transform;
}
/* 点击涟漪 */
.click-effect {
position: fixed;
background: rgba(128, 128, 128, 0.3);
border-radius: 50%;
transform: scale(0);
animation: clickAnimation 0.6s ease-out;
pointer-events: none;
z-index: 9999999;
}
@keyframes clickAnimation {
0% { transform: scale(0); opacity: 0.8; }
100% { transform: scale(4); opacity: 0; }
}
/* 移动端降级 */
@media (max-width: 1023px) {
html.custom-cursor-active,
html.custom-cursor-active * { cursor: auto !important; }
#g-pointer { display: none; }
}
JavaScript
var MIN_WIDTH = 1024;
var gPointer = null;
var isEnabled = false;
var isPressing = false;
function buildTransform(x, y) {
var t = "translate(" + (x - 8.5) + "px, " + (y - 8.5) + "px)";
if (isPressing) t += " scale(0.15)";
return t;
}
function onMouseMove(e) {
gPointer.style.transform = buildTransform(e.clientX, e.clientY);
}
function onMouseDown(e) {
if (!isEnabled) return;
isPressing = true;
gPointer.style.transform = buildTransform(e.clientX, e.clientY);
}
function onMouseUp(e) {
if (!isEnabled) return;
isPressing = false;
gPointer.style.transform = buildTransform(e.clientX, e.clientY);
// 创建涟漪
var el = document.createElement("div");
el.className = "click-effect";
el.style.left = (e.clientX - 17) + "px";
el.style.top = (e.clientY - 17) + "px";
el.style.width = "34px";
el.style.height = "34px";
document.body.appendChild(el);
el.addEventListener("animationend", function () { el.remove(); });
}
效果激活时同步屏蔽右键菜单和文本选中,避免原生光标残留导致体验割裂:
function onContextMenu(e) { e.preventDefault(); }
function onSelectStart(e) { e.preventDefault(); }
注意点
- scale变换原点:不推荐用CSS
scale属性做按下缩放,它的变换原点默认是元素在文档流中的位置(top:0; left:0),而非当前屏幕位置。应写在同一个transform字符串中,函数从左到右执行,先translate定位再scale收缩,圆心不会偏移 - 涟漪定位:
left/top用e.clientX - 半径使元素中心对准鼠标位置,再用animationend事件确保动画结束后移除DOM节点 - 离线/重连处理:PJAX或SPA导航后需要重新激活效果,监听
astro:page-load或swup的content:replace事件,执行deactivate()+activate()重新绑定 - 移动端降级:
< 1024px时完全禁用,恢复系统光标,解绑所有事件,不留副作用 - 跨域iframe限制:Giscus等评论区通过跨域iframe加载,浏览器出于安全隔离不允许父页面的CSS规则穿透到iframe内部,因此鼠标移入评论区后会恢复系统光标。这个问题无法从父页面修复,属于浏览器安全策略限制
可调整参数
| 参数 | 位置 | 说明 |
|---|---|---|
- 8.5 | JS偏移量 | 灰圆相对鼠标的偏移像素,修改它微调对齐 |
scale(0.15) | JS buildTransform | 按下时灰圆缩放倍率 |
0.1s | CSS transition | 灰圆跟随延迟,越大越粘滞 |
34px / scale(4) | JS/CSS keyframe | 涟漪初始尺寸和扩散倍率 |
1023px | CSS media query | 效果生效的屏幕宽度阈值 |
完整代码
完整实现在两个文件中:
src/styles/custom-cursor.css— 所有样式和keyframe动画src/components/features/CustomCursor.astro— JS逻辑和Astro组件封装
在 Layout.astro 中引入组件即可全局生效
import CustomCursor from "@components/features/CustomCursor.astro";
<CustomCursor />
实现时建议直接从 transform 方案开始。SVG光标和div元素走的是两套渲染坐标系,直接对齐很难,需要在JS里加一个偏移量来手动校正