
网页鼠标特效代码怎么写:从基础实现到自定义效果
网页鼠标特效可以是一个跟随指针移动的圆点、一串点击粒子、一条光标轨迹,也可以是悬停按钮时出现的高亮。它们通常由 CSS 绘制外观,再由 JavaScript 读取指针位置并更新样式。写起来不难,真正需要考虑的是:特效是否妨碍用户操作、触屏设备上如何处理、动画是否足够流畅,以及代码是否只做了它应该做的事。
本文从一个可运行的基础示例开始,逐步介绍常见效果、自定义方法和排错思路。示例不依赖第三方库,可以复制到本地 HTML 文件中测试。
一、先确定特效适不适合这个页面
鼠标特效更适合用于视觉表达明确、以桌面端指针操作为主的页面,例如作品集、活动专题页、互动展示页和游戏界面。柔和的光标跟随可以强化页面氛围,按钮附近的微动效可以提示交互区域,点击反馈也能让操作更容易被感知。
但特效并非越多越好。面向阅读、办公、表单填写或数据操作的页面,用户更关心内容和准确点击。大面积遮挡指针、延迟跟随、不断散落粒子等效果,可能让页面显得嘈杂,也可能增加误操作。设计前可以先问三个问题:
- 这个效果是否能提供状态反馈或表达页面风格,而不只是持续吸引注意?
- 关闭 JavaScript、开启减少动态效果,或使用触屏时,页面是否仍然可用?
- 效果是否会遮挡文本、按钮、输入框、系统光标或其他重要内容?
若答案不明确,先做局部、短暂、低对比度的反馈,通常比全屏跟随光标更稳妥。
二、最简单的 CSS 鼠标悬停效果
并不是所有“鼠标特效”都需要读取鼠标坐标。按钮悬停、卡片高亮等效果可以仅使用 CSS 的 :hover,代码更少,也不需要持续运行 JavaScript:
<button class="demo-button" type="button">移动鼠标到这里</button>
<style>
.demo-button {
padding: 12px 20px;
border: 1px solid #7c6cff;
border-radius: 8px;
background: #fff;
color: #342b78;
cursor: pointer;
transition: background-color 180ms ease, transform 180ms ease;
}
.demo-button:hover {
background: #eeeaff;
transform: translateY(-2px);
}
.demo-button:focus-visible {
outline: 3px solid #342b78;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.demo-button {
transition: none;
}
}
</style>
:hover 表示指针悬停状态,不等同于键盘焦点。按钮仍应保留可见的焦点样式,让使用键盘的人知道当前焦点在哪里。CSS 动画也要考虑用户的减少动态效果偏好;示例通过 prefers-reduced-motion 移除了过渡,而不是假设所有人都希望看到动画。
三、完整示例:制作跟随鼠标的光标圆点
下面的例子创建一个固定在视口上的装饰圆点。JavaScript 使用 pointermove 监听指针移动,并通过 transform 更新位置。圆点本身设置 pointer-events: none,不会挡住下面的按钮和链接;窄屏、触屏为主的设备,以及启用了减少动态效果的用户,都不会显示这个跟随效果。
将以下内容保存为 cursor-demo.html,直接用浏览器打开即可运行:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>鼠标跟随示例</title>
<style>
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 48px;
font-family: system-ui, sans-serif;
color: #252333;
background: #f6f4ff;
}
.cursor-dot {
position: fixed;
z-index: 10;
top: 0;
left: 0;
width: 18px;
height: 18px;
border: 2px solid #6554d9;
border-radius: 50%;
background: rgb(101 84 217 / 18%);
pointer-events: none;
transform: translate3d(-100px, -100px, 0);
will-change: transform;
}
.card {
max-width: 520px;
margin-top: 32px;
padding: 24px;
border-radius: 14px;
background: white;
box-shadow: 0 8px 28px rgb(30 20 80 / 8%);
}
a, button {
color: #4636b5;
}
@media (hover: none), (pointer: coarse), (max-width: 700px) {
.cursor-dot {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cursor-dot {
display: none;
}
}
</style>
</head>
<body>
<h1>指针跟随效果</h1>
<p>移动鼠标观察圆点。触屏设备不会显示跟随装饰。</p>
<button type="button">普通按钮</button>
<div class="card">
<p>装饰圆点不接管点击。<a href="#details">这是一个链接</a>。</p>
</div>
<div class="cursor-dot" aria-hidden="true"></div>
<script>
(() => {
const dot = document.querySelector(".cursor-dot");
if (!dot) return;
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
if (reduceMotion.matches) return;
let latestX = 0;
let latestY = 0;
let frameRequested = false;
function render() {
dot.style.transform =
`translate3d(${latestX}px, ${latestY}px, 0) translate(-50%, -50%)`;
frameRequested = false;
}
function onPointerMove(event) {
if (event.pointerType === "touch") return;
latestX = event.clientX;
latestY = event.clientY;
if (!frameRequested) {
frameRequested = true;
requestAnimationFrame(render);
}
}
window.addEventListener("pointermove", onPointerMove, { passive: true });
})();
</script>
</body>
</html>
代码中几个容易忽略的细节
position: fixed让圆点相对于视口定位;clientX和clientY也是相对于视口的坐标,两者配套使用。不要直接把页面滚动坐标与视口坐标混在一起。pointer-events: none让装饰元素不参与指针命中测试。否则,即使圆点看起来很轻,也可能挡住它下面的链接或按钮。requestAnimationFrame将一帧内多次到来的位置更新合并到浏览器下一次绘制时处理。示例只在有新位置且尚未请求帧时安排更新。will-change: transform是给浏览器的优化提示,不是性能保证。只应在确实会持续变化的元素上谨慎使用;页面中大量元素长期设置它,反而可能增加资源占用。- 圆点通过
aria-hidden="true"标记为装饰内容。它不提供操作信息,屏幕阅读器不需要读出它。
四、让跟随效果更柔和:缓动与平滑追踪
上一例让圆点直接到达最新坐标。如果希望它有一点惯性,可以让实际位置逐步接近目标位置。下面的函数适用于已创建的装饰元素:指针移动时只更新目标坐标,动画帧中再逐步推进当前位置。
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
let running = false;
function onMove(event) {
if (event.pointerType === "touch") return;
targetX = event.clientX;
targetY = event.clientY;
if (!running) {
running = true;
requestAnimationFrame(animate);
}
}
function animate() {
currentX += (targetX - currentX) * 0.18;
currentY += (targetY - currentY) * 0.18;
dot.style.transform =
`translate3d(${currentX}px, ${currentY}px, 0) translate(-50%, -50%)`;
const distance = Math.hypot(targetX - currentX, targetY - currentY);
if (distance > 0.5) {
requestAnimationFrame(animate);
} else {
currentX = targetX;
currentY = targetY;
dot.style.transform =
`translate3d(${currentX}px, ${currentY}px, 0) translate(-50%, -50%)`;
running = false;
}
}
window.addEventListener("pointermove", onMove, { passive: true });
系数 0.18 是视觉调节参数:较小会更慢、更拖尾,较大会更快贴近目标。它不是固定的浏览器规范值,也不应被描述成适用于所有设备的最佳值。应结合圆点尺寸、页面内容和实际设备测试。若停止移动后仍持续运行动画,通常是没有判断当前位置是否已经接近目标。
五、点击粒子:使用短暂元素表现一次反馈
点击粒子适合用于按钮或画布上的短反馈,不适合无限累积。下面的示例在页面指针按下时添加一个圆点,并在动画结束后删除它。示例使用独立的装饰层,避免临时元素影响页面布局。
<style>
.click-layer {
position: fixed;
inset: 0;
z-index: 20;
overflow: hidden;
pointer-events: none;
}
.click-particle {
position: absolute;
width: 12px;
height: 12px;
border: 2px solid #6554d9;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.5);
opacity: 1;
animation: particle-out 420ms ease-out forwards;
}
@keyframes particle-out {
to {
transform: translate(-50%, -50%) scale(3);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.click-particle {
animation: none;
opacity: 0;
}
}
</style>
<div class="click-layer" aria-hidden="true"></div>
<script>
const layer = document.querySelector(".click-layer");
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
window.addEventListener("pointerdown", (event) => {
if (!layer || reducedMotion.matches) return;
if (event.pointerType === "touch") return;
const particle = document.createElement("span");
particle.className = "click-particle";
particle.style.left = `${event.clientX}px`;
particle.style.top = `${event.clientY}px`;
layer.append(particle);
particle.addEventListener("animationend", () => {
particle.remove();
}, { once: true });
}, { passive: true });
</script>
要注意,动画被关闭或浏览器未触发预期的动画结束事件时,单靠 animationend 清理元素可能不够。上例已在减少动态效果时不创建粒子;对需要支持更多运行环境的复杂动画,还可以增加一个有明确上限的备用清理机制。无论采用哪种方式,都应避免节点无限增长。
六、常见自定义方式
调整尺寸、颜色和透明度
优先从 CSS 入手。圆点的 width、height、border、background 和 border-radius 决定大部分观感。较大的实心元素会遮住内容;透明填充和细边框通常更克制。颜色还要考虑页面对比度,尤其不要覆盖原有焦点指示器。
悬停可交互元素时改变样式
可以监听 pointerover 与 pointerout,也可以在特效元素或页面上切换一个 CSS 类。更简单、能纯 CSS 完成的状态应优先用 CSS。若用 JavaScript,不要只根据元素的标签名判断“可交互”,因为项目可能包含自定义组件;可以为需要反馈的区域明确添加类名或 data-cursor 属性。
把效果限定在指定区域
不需要在整个文档监听事件时,可把监听器绑在具体容器上。容器滚动、嵌套定位、CSS 变换等场景会影响坐标换算:视口坐标不能不经处理就当作容器内部坐标。若使用容器的本地坐标,通常需要结合容器的边界矩形和滚动位置进行换算,并在滚动或尺寸变化时重新检查。
切换启用状态
可以通过类名或数据属性统一控制效果的开启与关闭,也可以在组件卸载时移除监听器。示例中的即时函数适用于整页一次性演示;在前端组件框架中,应把注册与清理放在组件生命周期中。若通过媒体查询在运行时切换效果,也要同时更新 CSS 显示状态和 JavaScript 行为,避免元素隐藏了但事件仍在无意义地工作。
七、触屏、键盘与无障碍边界
触屏设备没有传统意义上持续移动的鼠标指针。虽然指针事件可以统一处理鼠标、触控笔和触摸输入,但不能假设所有设备都有相同的指针能力。示例通过 event.pointerType 排除触摸输入,并使用 CSS 媒体特性 hover、pointer 做显示筛选。混合设备可能同时具备鼠标和触屏,因此媒体查询与事件判断是互补关系,不是相互替代。
跟随光标的装饰不应取代系统光标。除非产品有明确且经过可用性评估的理由,不要使用 cursor: none 隐藏原生光标;视觉圆点可能有延迟、颜色不清晰,也可能在某些状态下消失。更不应把鼠标移动作为完成任务的唯一方式:核心操作必须有键盘可达的路径,按钮需保留焦点样式,交互状态也不能只靠颜色表达。
prefers-reduced-motion 可以作为减少非必要动画的依据,但它不等于自动关闭所有动态效果的强制规范。开发者应根据动效内容提供适当的降级方式。本文示例选择在该偏好下隐藏跟随圆点和点击动画;其他项目也可以改为静态装饰或缩短过渡。
八、性能与浏览器行为
页面流畅度不仅取决于每秒更新多少次,也取决于每次更新做什么。跟随元素的位置动画优先尝试 transform,避免在高频事件中不断修改会触发布局的属性,例如宽高、边距或元素流中的位置。不要在每次指针事件中遍历大量 DOM、执行昂贵的计算,或反复创建和删除大量节点。
pointermove 可能频繁触发。将渲染合并到 requestAnimationFrame 通常有助于控制更新节奏,但这并不意味着可以忽略昂贵计算。事件监听器的 passive: true 表示监听器不会调用 preventDefault() 来取消默认行为;对于只读取坐标的监听器,这是合适的声明。若确实需要取消某类默认行为,就不能把相应监听器设为被动,并且应谨慎评估对滚动和触控操作的影响。
浏览器窗口离开视口、标签页进入后台或设备帧率变化时,动画执行节奏会改变。不要依赖“每帧移动固定像素”来定义严格的时间速度;需要基于时间的动画时应使用动画帧时间戳计算间隔。装饰效果通常不需要追求物理仿真,简单、稳定、低开销更重要。
九、常见错误与排查方法
- 圆点挡住了按钮:检查是否设置了
pointer-events: none,以及装饰层的层级是否过高。不要为了让效果可见而随意提高整个页面的z-index。 - 圆点位置偏移:核对定位方式与坐标来源。
clientX/clientY对应视口坐标;页面坐标需要考虑滚动,容器坐标还需要考虑容器边界和滚动。元素若处于带有特定变换的祖先上下文中,也要检查固定定位的参照环境。 - 鼠标一动页面就卡:检查是否在事件中反复读写布局属性、遍历整个 DOM,或每次移动都创建新元素。先缩小效果范围,再用开发者工具观察性能。
- 粒子越来越多:确保动画结束后删除节点,并检查被关闭的动画、页面切换或脚本报错时是否仍能清理。为同时存在的粒子设置合理上限也有帮助。
- 触屏上出现奇怪行为:不要把触摸事件等同于鼠标悬停。区分
pointerType,为触屏提供不依赖光标的反馈。 - 禁用动画后页面失效:动画应是装饰,不应承担提交成功、错误提醒等唯一反馈。重要状态还要通过文本、结构或其他可访问形式表达。
- 自定义光标与浏览器缩放冲突:不要把坐标误当成页面的 CSS 像素缩放结果,也不要假定设备像素比等于 1。浏览器事件提供的坐标可用于 CSS 布局定位;绘制到高分辨率画布时才需要另行处理画布的像素尺寸与 CSS 尺寸。
十、安全与隐私:特效代码能做什么,不能做什么
读取 pointermove 坐标本身只会让当前页面知道指针在页面中的位置,方便绘制视觉反馈。它不会因此获得鼠标在整个操作系统中的位置,也不会自动读取键盘输入或绕过浏览器权限。不要把这种页面内交互描述成“监控用户所有操作”或“获取电脑鼠标轨迹”。
如果将指针数据记录、发送到服务器或用于分析,那就不再只是本地视觉特效,必须重新评估数据收集目的、必要性、告知方式、保存期限与适用的隐私要求。一般的光标跟随没有必要把坐标上传。也不要在此类代码中偷偷记录按键、表单内容或剪贴板数据。
示例使用固定的 CSS 类名、数字坐标和 textContent 以外的安全 DOM 创建方式,没有把用户输入拼接进 HTML。扩展代码时,避免将不可信字符串直接交给 innerHTML;若确实需要插入动态文本,优先使用 textContent。外部脚本和样式应来自可信来源,并按项目的内容安全策略配置。鼠标动画不是安全防护,也不能替代权限控制、输入校验或防注入措施。
十一、从示例改成生产代码的检查清单
- 特效仅用于装饰或辅助反馈,核心操作不依赖它。
- 原生鼠标指针保留可见,焦点和交互状态有独立提示。
- 装饰层不拦截点击,且没有覆盖重要内容。
- 触屏设备有合理降级,减少动态效果偏好得到尊重。
- 高频事件只保存必要状态,绘制在合适的时机进行。
- 临时 DOM 元素有清理方式,监听器在不再需要时解除。
- 不收集、传输与视觉效果无关的用户数据,也不插入未经校验的 HTML。
实现网页鼠标特效时,可以按“先做静态样式,再加事件,再优化动画,最后验证边界”的顺序推进。简单悬停效果优先使用 CSS;确实需要跟随坐标时,再用指针事件更新装饰元素。把交互范围收小、让系统光标和键盘操作保持可靠,往往比增加更多粒子和动画更能提升页面体验。



