You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 11:10:13