如何实现粘性Section进入视口时的联动滚动效果?
实现粘性Section内部内容随Body滚动联动的方案
核心思路
- 用
IntersectionObserver判断粘性section是否处于激活状态(即sticky定位生效,section固定在视口内) - 监听
wheel事件(而非scroll事件),该事件可直接获取滚动增量deltaY,且TypeScript的WheelEvent类型原生包含该属性 - 当section激活时,阻止body默认滚动,将滚动增量传递给内部
.content元素;同时判断.content是否已滚动到边界,若到达边界则恢复body滚动
1. 解决TypeScript类型问题
你遇到的deltaY属性不存在的问题,是因为误将事件类型指定为通用Event。只需在监听wheel事件时,明确事件类型为WheelEvent即可:
document.addEventListener('wheel', (e: WheelEvent) => { // 此处可正常访问e.deltaY });
2. 监听Section的粘性激活状态
通过IntersectionObserver结合getBoundingClientRect精准判断section是否进入sticky生效状态:
const section = document.querySelector('section') as HTMLElement; const content = section.querySelector('.content') as HTMLElement; let isStickyActive = false; const observer = new IntersectionObserver( ([entry]) => { const rect = section.getBoundingClientRect(); // 当section固定在top:80px位置且未完全离开视口时,判定为激活状态 isStickyActive = Math.abs(rect.top - 80) < 1 && entry.isIntersecting; }, { root: null, threshold: [0, 0.5, 1], rootMargin: '-80px 0px' // 匹配sticky的top值,调整交叉判断边界 } ); observer.observe(section);
3. 实现滚动联动逻辑
在wheel事件中,根据激活状态控制滚动目标,并处理边界判断:
document.addEventListener('wheel', (e: WheelEvent) => { if (!isStickyActive) return; // 判断.content是否到达滚动边界 const isAtTop = content.scrollTop === 0; const isAtBottom = content.scrollTop + content.clientHeight >= content.scrollHeight - 1; // 留1px误差避免渲染精度问题 // 若已到达边界,恢复body原生滚动 if ((e.deltaY < 0 && isAtTop) || (e.deltaY > 0 && isAtBottom)) { return; } // 阻止body滚动,转而滚动内部.content e.preventDefault(); content.scrollBy({ top: e.deltaY, behavior: 'auto' // 保持原生滚动速度 }); });
完整代码示例
HTML
<section> <div class="content"> <div class="child">Item 1</div> <div class="child">Item 2</div> <!-- 省略其余child元素 --> <div class="child">Item 21</div> </div> </section>
CSS
section { min-height: 682px; position: sticky; top: 80px; border: 1px solid #ccc; } .content { height: 618px; overflow-y: scroll; display: flex; flex-direction: column; gap: 5px; } .child { height: 120px; background: #f5f5f5; border: 1px solid #eee; display: flex; align-items: center; justify-content: center; } /* 测试用:给body添加足够高度 */ body { height: 3000px; padding-top: 100px; }
TypeScript
const initStickyScroll = () => { const section = document.querySelector('section') as HTMLElement; const content = section.querySelector('.content') as HTMLElement; if (!section || !content) return; let isStickyActive = false; const observer = new IntersectionObserver( ([entry]) => { const rect = section.getBoundingClientRect(); isStickyActive = Math.abs(rect.top - 80) < 1 && entry.isIntersecting; }, { root: null, threshold: [0, 0.5, 1], rootMargin: '-80px 0px' } ); observer.observe(section); const handleWheel = (e: WheelEvent) => { if (!isStickyActive) return; const isAtTop = content.scrollTop === 0; const isAtBottom = content.scrollTop + content.clientHeight >= content.scrollHeight - 1; if ((e.deltaY < 0 && isAtTop) || (e.deltaY > 0 && isAtBottom)) { return; } e.preventDefault(); content.scrollBy({ top: e.deltaY, behavior: 'auto' }); }; document.addEventListener('wheel', handleWheel); // 提供清理监听的方法 return () => { observer.disconnect(); document.removeEventListener('wheel', handleWheel); }; }; // 初始化功能 initStickyScroll();
关键细节说明
- 选用
wheel事件:相比scroll事件,它能在滚动触发时直接获取增量,更适合控制滚动目标切换 - 边界判断留误差:浏览器渲染精度问题可能导致
scrollTop + clientHeight与scrollHeight存在微小差值,减1px可避免误判 rootMargin配置:匹配sticky的top值,让交叉判断更精准地捕捉section进入粘性状态的时机
内容的提问来源于stack exchange,提问作者Antonio Longo
相关产品推荐
相关产品推荐

