如何实现Todoist官网特殊Scroll Progress Animation滚动动画效果
实现Todoist式区域锁定滚动动画
核心原理
当页面滚动到目标区域时,锁定整个页面的滚动,仅允许目标区域内部的内容滚动;当内部内容滚动到边界(顶部/底部)后,恢复页面的正常滚动。
实现步骤
1. HTML结构
区分普通页面内容和特殊滚动区域,特殊区域内部嵌套可滚动容器:
<!-- 页面上方普通内容 --> <section class="page-section"> <h2>页面顶部内容</h2> <p>向下滚动到特殊区域查看效果...</p> <!-- 填充足够多内容让页面可滚动 --> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> </section> <!-- 特殊滚动锁定区域 --> <section class="sticky-container"> <div class="inner-scroll" tabindex="0"> <h2>特殊滚动区域</h2> <p>这里的内容会单独滚动,页面整体暂时锁定...</p> <!-- 填充足够长的内部内容 --> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> </div> </section> <!-- 页面下方普通内容 --> <section class="page-section"> <h2>页面底部内容</h2> <p>特殊区域滚动完成后,可继续滚动到这里...</p> </section>
2. CSS样式
让特殊区域固定在视口内,内部容器设置可滚动属性:
/* 普通页面区块 */ .page-section { height: 100vh; padding: 2rem; background-color: #f8f9fa; } /* 特殊区域容器:滚动到后固定在视口 */ .sticky-container { position: sticky; top: 0; height: 100vh; overflow: hidden; /* 防止内部内容溢出页面 */ } /* 内部可滚动容器 */ .inner-scroll { height: 100%; overflow-y: auto; padding: 2rem; background-color: #ffffff; outline: none; /* 移除聚焦时的默认边框 */ } /* 可选:隐藏内部滚动条(模拟Todoist风格) */ .inner-scroll::-webkit-scrollbar { display: none; } .inner-scroll { -ms-overflow-style: none; scrollbar-width: none; }
3. JavaScript逻辑
核心是监听页面滚动和内部容器滚动,控制页面滚动的锁定与解锁:
// 获取DOM元素 const stickyContainer = document.querySelector('.sticky-container'); const innerScroll = document.querySelector('.inner-scroll'); let isStickyLocked = false; // 页面滚动监听:判断是否进入特殊区域 function checkStickyState() { const containerRect = stickyContainer.getBoundingClientRect(); // 判断是否完全进入视口:顶部<=0,底部>=视口高度 const isInViewport = containerRect.top <= 0 && containerRect.bottom >= window.innerHeight; if (isInViewport && !isStickyLocked) { isStickyLocked = true; // 锁定页面滚动 document.body.style.overflow = 'hidden'; // 聚焦内部容器,支持键盘滚动 innerScroll.focus(); } else if (!isInViewport && isStickyLocked) { isStickyLocked = false; // 解锁页面滚动 document.body.style.overflow = ''; } } // 内部容器滚动监听:判断是否到达边界,解锁页面 function handleInnerScroll() { if (!isStickyLocked) return; // 到达底部的判断(留10px误差避免滚动精度问题) const atBottom = innerScroll.scrollTop + innerScroll.clientHeight >= innerScroll.scrollHeight - 10; // 到达顶部的判断 const atTop = innerScroll.scrollTop <= 10; if (atBottom || atTop) { isStickyLocked = false; document.body.style.overflow = ''; // 手动触发页面滚动,让页面继续滚动 window.scrollBy(0, atBottom ? 1 : -1); } } // 移动端触摸事件处理:防止边界外的滚动穿透 function handleTouchMove(e) { if (!isStickyLocked) return; const touchStartY = e.touches[0].clientY; const touchEndY = e.changedTouches[0].clientY; const scrollingDown = touchStartY > touchEndY; const scrollingUp = touchStartY < touchEndY; // 如果在底部还想下滑,或顶部想上滑,阻止默认行为 if ((isAtBottom() && scrollingDown) || (isAtTop() && scrollingUp)) { e.preventDefault(); } } // 辅助函数:判断是否到达顶部/底部 function isAtBottom() { return innerScroll.scrollTop + innerScroll.clientHeight >= innerScroll.scrollHeight - 10; } function isAtTop() { return innerScroll.scrollTop <= 10; } // 绑定事件监听 window.addEventListener('scroll', checkStickyState); innerScroll.addEventListener('scroll', handleInnerScroll); innerScroll.addEventListener('touchmove', handleTouchMove, { passive: false }); // 页面加载时初始化状态 window.addEventListener('load', checkStickyState);
关键注意事项
- 边界判断误差:留10px的误差值,避免因滚动精度问题导致状态切换异常
- 移动端兼容性:
touchmove事件需要设置{ passive: false }才能阻止默认行为 - 键盘支持:给内部容器添加
tabindex="0",确保键盘可触发滚动 - 内容长度判断:如果内部内容高度小于视口高度,不需要开启锁定逻辑,可提前判断并跳过
内容的提问来源于stack exchange,提问作者Sanjay K
相关产品推荐
相关产品推荐

