用document.addEventListener('mousemove')监听事件并取clientX/clientY获取视口坐标,通过document.documentElement.style.setProperty()更新:root下的--x、--y变量,再在radial-gradient(circle at var(--x) var(--y),...)中动态定位聚光灯圆心,遮罩层需设pointer-events:none且推荐用伪元素实现。

如何用 document.addEventListener('mousemove') 获取鼠标实时坐标
核心是监听 mousemove 事件,从事件对象里取 clientX 和 clientY —— 它们是相对于视口左上角的像素值,直接可用,无需换算。
注意别用 pageX/pageY,滚动时会偏移;也别用 offsetX/offsetY,它们相对于目标元素,而聚光灯通常要覆盖整个 body 或容器。
实操建议:
- 绑定到
document而非某个具体元素,确保鼠标在页面任意位置移动都能捕获 - 加
{ passive: true }提升滚动性能(但注意:若后续需调用event.preventDefault()就不能设为 passive) - 避免高频触发导致卡顿:可节流(如
requestAnimationFrame包裹),但聚光灯对延迟不敏感,多数场景直接更新即可
如何用 style.setProperty() 动态更新 CSS 变量
CSS 变量必须定义在某个作用域的 :root 或元素的 style 上,JS 才能改。聚光灯一般需要全局生效,所以优先操作 document.documentElement(即 :root)。
常见错误:写成 document.body.style.setProperty('--x', x) —— 这样变量只在 body 元素上生效,子元素无法继承;而聚光灯遮罩层常是 body 的子元素,必须确保变量在根级可访问。
立即学习“前端免费学习笔记(深入)”;
实操建议:
- 初始化时在
:root声明默认值,例如:root { --x: 50vw; --y: 50vh; },避免 JS 未执行前样式出错 - 更新时统一用
document.documentElement.style.setProperty('--x', e.clientX + 'px'),单位不能漏 - 不要拼字符串赋值(如
style.cssText += '--x: ...'),会覆盖已有内联样式
如何用 radial-gradient 和 CSS 变量实现聚光灯遮罩
聚光灯本质是一个中心透明、四周渐变黑的圆形遮罩,用 background 实现最轻量。关键在于把 --x 和 --y 变量注入到 radial-gradient 的圆心位置。
注意浏览器兼容性:所有现代浏览器都支持在 radial-gradient 中使用 CSS 变量,但变量必须是合法的长度值(如 120px),不能是纯数字或带空格的字符串。
实操建议:
- 遮罩层推荐用伪元素(如
body::before)或固定定位的空div,避免影响文档流 - 渐变写法示例:
background: radial-gradient(circle at var(--x) var(--y), transparent 50px, rgba(0,0,0,0.8) 150px) -
50px是光圈半径,150px是过渡结束位置,两个值共同控制“光斑大小”和“边缘柔和度”,调参比写 JS 更快 - 若发现光斑跳动,检查是否忘了给遮罩层加
pointer-events: none,否则鼠标事件会被遮罩拦截,导致坐标停止更新
为什么 transform: translate(-50%, -50%) 不适合聚光灯定位
有人试图用绝对定位 + left/top 加 transform 居中光斑,但这会让坐标逻辑变复杂:你得先减去半宽高,再算偏移,而且响应式下光斑尺寸变化时还得重算。CSS 变量配合 radial-gradient 的 at 语法直接锚定圆心,语义清晰、无副作用。
更隐蔽的问题是:transform 会创建新的层叠上下文和包含块,可能干扰遮罩层与其他 fixed 元素的层级关系,尤其在有 z-index 嵌套的项目里容易踩坑。
实操建议:
- 坚持用
radial-gradient的at参数驱动圆心,这是最接近“所见即所得”的方式 - 如果真要用定位方案(比如需要动画过渡光斑位置),务必给遮罩层加
will-change: transform防止重排,但聚光灯效果本身并不需要过渡 - 移动端要注意
touchmove事件需额外监听,且clientX/Y在触摸事件中同样可用,无需切换 API
pointer-events、或者忘了给 :root 写初始值——这几个点一错,整个效果就静默失效,连报错都没有。


















