← 返回博客

NOTE / LOG

给博客添加自定义鼠标效果

基于 shiyin.cafe 实现了三层自定义鼠标效果:SVG 光标替换、延迟跟随灰圆、点击涟漪。本文拆解实现方案和关键细节

CSSJavaScript鼠标效果前端教程

效果拆解

整个鼠标效果由三层组成,逐层叠加:

层级实现方式作用
1cursor: url(data:image/svg+xml...) 替换系统光标白色小圆点代替默认箭头,零延迟
2<div id="g-pointer"> 跟随鼠标移动灰色半透明圆,0.1s 过渡延迟产生粘滞感
3.click-effect 动画元素鼠标松开时扩散涟漪,动画结束后移除

关键视觉点是第二层的延迟跟随——白色光标在鼠标实际位置,灰色圆因为 transition: transform 0.1s ease会略微滞后,快速移动时两层错开,产生层次感

方案选择

定位跟随元素有两种方式,性能差异明显:

方案触发流程性能
left / topLayout → 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/tope.clientX - 半径 使元素中心对准鼠标位置,再用 animationend事件确保动画结束后移除DOM节点
  • 离线/重连处理:PJAX或SPA导航后需要重新激活效果,监听 astro:page-loadswupcontent:replace事件,执行 deactivate() + activate()重新绑定
  • 移动端降级< 1024px时完全禁用,恢复系统光标,解绑所有事件,不留副作用
  • 跨域iframe限制:Giscus等评论区通过跨域iframe加载,浏览器出于安全隔离不允许父页面的CSS规则穿透到iframe内部,因此鼠标移入评论区后会恢复系统光标。这个问题无法从父页面修复,属于浏览器安全策略限制

可调整参数

参数位置说明
- 8.5JS偏移量灰圆相对鼠标的偏移像素,修改它微调对齐
scale(0.15)JS buildTransform按下时灰圆缩放倍率
0.1sCSS transition灰圆跟随延迟,越大越粘滞
34px / scale(4)JS/CSS keyframe涟漪初始尺寸和扩散倍率
1023pxCSS 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里加一个偏移量来手动校正