
本文介绍如何利用原生 wheel 事件精准捕获鼠标滚轮方向,替代无效的 scroll 监听,在无滚动条的全屏堆叠布局中实现基于滚轮的页面切换动画(支持 Framer Motion 动画逻辑)。
本文介绍如何利用原生 `wheel` 事件精准捕获鼠标滚轮方向,替代无效的 `scroll` 监听,在无滚动条的全屏堆叠布局中实现基于滚轮的页面切换动画(支持 framer motion 动画逻辑)。
在 React 应用中,当多个
以 position: absolute 堆叠、整体容器本身不可滚动(即未触发原生 scroll 事件)时,常见的 window.addEventListener(‘scroll’, …) 将完全失效——因为页面并未发生滚动,只是用户习惯性地滚动鼠标滚轮。此时,真正应监听的是更底层的 wheel 事件:它在任意可聚焦元素上(包括 或任意 div)均可触发,且携带精确的方向与幅度信息(deltaY),与是否启用滚动条无关。
✅ 正确方案:监听 wheel 事件并解析 deltaY
wheel 事件的 event.deltaY 是核心判断依据:
- deltaY > 0:表示向下滚动(常见于鼠标滚轮向下、触控板下滑);
- deltaY 向上滚动(常见于鼠标滚轮向上、触控板上滑);
- 数值大小反映滚动强度,可用于设置灵敏度阈值,避免误触发。
以下为关键改造代码(已整合进你的 Framer Motion 页面动画逻辑):
function Page({ bgColor = "red", isVisible, isDown, onWheel // ? 新增 prop:透传 wheel 处理函数 }) { return ( <motion.div style={{ height: '90vh', width: "100%", backgroundColor: bgColor, position: 'absolute', }} animate={{ opacity: isVisible ? 1 : [1, 0, 0], y: isVisible ? 0 : isDown ? [0, 90, 180, 0] // 向下切换:当前页先下移再回弹 : [0, -90, -180, 0] // 向上切换:当前页先上移再回弹 }} transition={{ delay: isVisible ? 0.15 : 0, duration: 0.5 }} onWheel={onWheel} // ? 绑定到 motion.div,确保事件被捕获 /> ); } function App() { const [currentPage, setCurrentPage] = useState(0); const [isScrollDirectionUp, setIsScrollDirectionUp] = useState(true); // ✅ 核心:wheel 事件处理器(带防抖与阈值) const handleWheel = useCallback((event: WheelEvent) => { event.preventDefault(); // 阻止默认行为(如页面意外滚动) const THRESHOLD = 80; // 滚动灵敏度阈值(单位:px),可根据体验调整 if (event.deltaY > THRESHOLD) { // 向下滚动 → 下一页 setIsScrollDirectionUp(false); setCurrentPage(prev => (prev + 1) % 4); } else if (event.deltaY < -THRESHOLD) { // 向上滚动 → 上一页 setIsScrollDirectionUp(true); setCurrentPage(prev => (prev - 1 + 4) % 4); // 避免负数取模问题 } }, []); const pagesArr = [ <Page bgColor="blue" isVisible={currentPage === 0} isDown={isScrollDirectionUp} onWheel={handleWheel} />, <Page bgColor="orange" isVisible={currentPage === 1} isDown={isScrollDirectionUp} onWheel={handleWheel} />, <Page bgColor="green" isVisible={currentPage === 2} isDown={isScrollDirectionUp} onWheel={handleWheel} />, <Page bgColor="red" isVisible={currentPage === 3} isDown={isScrollDirectionUp} onWheel={handleWheel} /> ]; return ( <div style={{ overflow: 'hidden', // 确保容器自身不产生滚动条 height: '100vh', position: 'relative' }} onWheel={handleWheel} // ? 也可绑定到根 div,更健壮(推荐) > <div style={{ position: "fixed", zIndex: 3 }}> <button onClick={() => { setIsScrollDirectionUp(true); setCurrentPage(prev => (prev - 1 + 4) % 4); }}>prev page</button> <button onClick={() => { setIsScrollDirectionUp(false); setCurrentPage(prev => (prev + 1) % 4); }}>next page</button> </div> {pagesArr} </div> ); }
⚠️ 注意事项与最佳实践
- event.preventDefault() 必须调用:否则在某些浏览器/设备上可能触发意外的页面滚动或缩放。
- 阈值(THRESHOLD)至关重要:deltaY 在不同设备(鼠标/触控板/触摸屏)上量级差异极大。建议从 50–100 起步,通过真实设备测试微调;过低易误触发,过高则响应迟钝。
- 防抖非必需但强烈推荐:高频 wheel 事件(尤其触控板惯性滚动)可能导致连续触发。可结合 useRef + setTimeout 实现简易节流:
const wheelTimeout = useRef<NodeJS.Timeout | null>(null); const handleWheel = useCallback((e: WheelEvent) => { if (wheelTimeout.current) clearTimeout(wheelTimeout.current); wheelTimeout.current = setTimeout(() => { // 执行页面切换逻辑 }, 100); }, []); - 跨浏览器兼容性:现代浏览器对 deltaY 符号定义一致(向下为正),但旧版 safari 可能需兼容 deltaMode。生产环境建议使用 normalizeWheel 工具函数统一标准化。
- 无障碍考量:纯滚轮导航对键盘/屏幕阅读器用户不友好,请务必保留按钮操作,并为 div 添加 tabIndex={0} 和 role=”application” 提升可访问性。
通过 wheel 事件精准捕获用户意图,你无需依赖 dom 滚动状态,即可在任意静态布局中复现流畅的“滚轮翻页”交互动效——这正是现代沉浸式单页应用(如产品介绍页、简历展示页)的关键技术支点。