下拉刷新动效不能只靠 transform: translateY(),因其会破坏原生滚动惯性与阻尼,导致无法自然回弹或触发页面整体拖拽;必须结合 scroll 事件、限制在 overscroll 区域,并用独立容器+transition 实现平滑回弹。

下拉刷新动效为什么不能只靠 transform: translateY()?
因为单纯用 transform: translateY() 拉动容器,会破坏原生滚动的惯性与阻尼,导致手指松开后无法自然回弹,甚至触发页面整体拖拽(尤其在 iOS Safari 中)。真正的下拉刷新动效必须和原生 scroll 事件联动,且只在 overscroll 区域生效——而这个区域默认不可见、不可直接样式化。
所以核心思路是:监听 touchstart/touchmove,计算下拉距离;仅当 scrollTop === 0 且向下滑动时,才允许更新一个「视觉层」的 transform;松手后靠 transition 回弹,而非靠 JS 动画帧控制。
如何用 transform + transition 实现平滑回弹?
关键在于把「下拉层」和「内容层」分离。不要对 body 或 html 做 transform,而是包裹一个独立的 refresh-wrapper 容器,并设置 will-change: transform 提升渲染层级:
<div class="refresh-wrapper"> <div class="refresh-indicator">↓ 下拉刷新</div> <div class="content">...</div> </div>
JS 控制时,只更新 .refresh-wrapper 的 transform: translateY(),同时给它加 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) —— 这个贝塞尔曲线比 ease-out 更贴合原生回弹感。
立即学习“前端免费学习笔记(深入)”;
- 下拉中:动态设置
style.transform = `translateY(${pullDistance}px)`,pullDistance最大建议限制在 80px 以内(避免过度拉伸) - 松手瞬间:若
pullDistance > 60,则设为translateY(60px)并触发刷新;否则设为translateY(0),靠 transition 自动回弹 - 刷新中:保持
translateY(60px),并替换.refresh-indicator文案为「加载中…」
为什么 touchmove 阻止默认行为要谨慎?
很多实现一上来就 event.preventDefault(),结果导致页面无法横向滚动、input 失焦、iOS 键盘弹不出。正确做法是:只在明确处于下拉状态(即 scrollTop === 0 && deltaY > 0)时才阻止,默认保留原生滚动能力。
还要注意 passive: false 选项必须显式声明,否则 Chrome 会忽略 preventDefault():
element.addEventListener('touchmove', handler, { passive: false });
- 不加
{ passive: false }→preventDefault()无效 → 下拉时页面仍会上滚 - 加了但没判断
scrollTop === 0→ 所有 touchmove 都被拦截 → 横向滑动卡片失效 - 推荐在
touchstart记录初始scrollTop和clientY,在touchmove中只对「起始位置在顶部 + 向下滑动」的情况干预
兼容 iOS Safari 的隐藏陷阱:overflow: scroll 不等于可 overscroll
iOS Safari 默认禁用非 body 元素的 overscroll 效果,即使你写了 overscroll-behavior-y: contain 也没用。真正能触发系统级下拉回弹的,只有 body 或 html 的滚动。所以如果你的容器用了 height: 100vh; overflow: auto,那它永远不会有原生 overscroll,也就无法靠 CSS 触发下拉动效。
解决方案只有两个:
- 放弃局部容器,让整个页面由
body滚动(最简单,但可能影响布局) - 坚持局部容器,就必须用 JS 模拟整个下拉-回弹-刷新流程,且所有动画必须用
transform+transition(不能用top或margin-top,否则触发重排)
后者更常见,但要注意:iOS 15+ 对 transform 在 fixed 定位元素上的渲染有 bug,如果 .refresh-indicator 是 position: fixed,请改用 position: absolute 并确保父容器 transform: translateZ(0) 强制硬件加速。


















