
本文介绍一种基于 CSS radial-gradient、伪元素和自定义 CSS 变量的高性能手电筒(flashlight)遮罩效果,无需额外 DOM 节点,通过动态更新 background-position 实现内容随鼠标实时“照亮”,兼顾视觉真实感与运行效率。
本文介绍一种基于 css `radial-gradient`、伪元素和自定义 css 变量的高性能手电筒(flashlight)遮罩效果,无需额外 dom 节点,通过动态更新 `background-position` 实现内容随鼠标实时“照亮”,兼顾视觉真实感与运行效率。
要实现真正意义上的“手电筒效果”——即在黑色背景上仅透过圆形光斑揭示下方内容,关键在于遮罩(masking)而非叠加(overlay)。原方案中直接添加 .flashlight div 并使用半透明径向渐变,本质是“加一层发光圆”,无法穿透遮盖层显露底层文字/图像;而正确思路是:将整个容器设为纯黑,再用一个带中心透明孔洞的深色径向渐变作为“遮罩层”,并通过 background-position 动态追踪鼠标位置,让“透明孔洞”始终对准光标。
以下为优化后的完整实现:
✅ 核心技术要点
- 零新增 DOM:利用 ::before 伪元素承担光照遮罩,避免频繁创建/移动 DOM 节点,减少重排(reflow);
- CSS 变量驱动定位:通过 --flashlight-x-pos / --flashlight-y-pos 控制遮罩层偏移,JS 仅负责更新变量值;
- 高效坐标计算:getBoundingClientRect() 提前执行一次(置于事件监听器外),避免 mousemove 中重复调用高开销 API;
- 语义化遮罩逻辑:radial-gradient 从透明 → 深色 → 纯黑,形成“发光中心 + 渐暗边缘 + 完全黑暗外围”的自然光效。
? 完整代码示例
/* CSS */
.box {
display: flex;
align-items: center;
justify-content: center;
cursor: none;
width: 100%;
padding: 5rem;
background-color: #000;
overflow: hidden;
position: relative;
/* 初始化遮罩偏移至屏幕外,实现初始隐藏 */
--flashlight-x-pos: -200px;
--flashlight-y-pos: -200px;
--flashlight-inset: -300px; /* 遮罩层扩展范围,确保边缘不裁切 */
}
.box::before {
content: '';
position: absolute;
inset: var(--flashlight-inset); /* 全覆盖遮罩层 */
/* 透明中心 → 暗绿色过渡 → 纯黑背景 */
background-image: radial-gradient(
circle,
transparent 0%,
rgba(47, 52, 2, 0.4) 60px,
black 70px,
black 100%
);
background-position: var(--flashlight-x-pos) var(--flashlight-y-pos);
background-repeat: no-repeat;
z-index: 10;
}
/* 可选:添加手电筒图标(同样用伪元素,轻量且可复用) */
.box::after {
content: '?';
font-size: 32px;
position: absolute;
transform: translate(var(--flashlight-x-pos), var(--flashlight-y-pos));
pointer-events: none;
z-index: 20;
}
// JavaScript
const box = document.querySelector(".box");
const rect = box.getBoundingClientRect(); // ✅ 提前获取,避免mousemove中重复计算
box.addEventListener("mousemove", (e) => {
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 偏移量以容器中心为基准,实现光斑居中跟随
box.style.setProperty("--flashlight-x-pos", `${x - rect.width / 2}px`);
box.style.setProperty("--flashlight-y-pos", `${y - rect.height / 2}px`);
});
box.addEventListener("mouseleave", () => {
// 鼠标离开时,将遮罩移出视口,恢复全黑
box.style.setProperty("--flashlight-x-pos", "-300px");
box.style.setProperty("--flashlight-y-pos", "-300px");
});
⚠️ 注意事项
- 渐变尺寸需匹配预期光斑大小:60px(透明区半径)和 70px(过渡结束)可根据设计调整,但需确保 70px
- transform: translate() 优于 left/top:伪元素定位推荐用 transform,触发 GPU 加速且不引发重排;
- 移动端适配:若需支持触摸设备,需额外监听 touchmove 并阻止默认行为,且注意 clientX/Y 在触摸事件中的获取方式;
- 性能监控:该方案 FPS 稳定在 60,远优于每帧操作 DOM 的旧方法;可通过 Chrome DevTools 的 Rendering 面板验证“Paint Flashing”是否仅发生在光斑区域。
此方案将光照效果从“视觉装饰”升级为“内容揭示机制”,既精准还原手电筒的物理特性,又具备生产环境所需的性能与可维护性。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











