overscroll-behavior
在移动端 H5 页面或部分 Web 组件的开发中,开发者们大概率遭遇过这样一个让人抓狂的场景: 页面中心弹出了一个小小的滚动弹窗(Modal),用户在弹窗里专心往下划。当弹窗内容到底了,如果手指还没离开屏幕继续往下扒拉——结果整个背后的页面内容开始跟着疯狂滚动!
这种因为局部滚动到头,导致把滚动事件“击穿”传递给下层父容器的现象,被称作滚动穿透(Scroll Chaining/Scroll Bleeding)。
在过去,为了制止这种“手贱”引起的联动反应,前端先驱们写出了无数篇名为《移动端彻底阻止弹窗滚动穿透》的文章。方案可谓惨烈:从通过 JS 监听 touchmove 并暴力 e.preventDefault(),到动态给 body 加 fixed 然后依靠 JS 算高度把页面死死钉住……不仅代码恶心,往往还会导致部分浏览器的原生弹性效果彻底报废。
直到 overscroll-behavior 带着圣光降临,几行 CSS 就优雅地平息了这一切。
浏览器的边界执念
我们要先理解,为什么浏览器非要触发这种“穿透”?
当我们用手指或者触控板滚动一个区域到达它的“边界(极值)”时,浏览器默认有两个连击动作:
- Scroll Chaining(滚动链传递):当前的地方滚不动了?没关系,我把你的滚动意图冒泡给你身后的父级有滚动条的祖宗容器,让它帮你滚。
- 触发原生手势反馈:如果连最外层也滚不动了,浏览器就会搬出系统级的花活——比如 iOS 那非常著名的果冻橡皮筋拉拽效果,或者 Android 浏览器的边缘发光效果,甚至是触发“下拉刷新”。
overscroll-behavior 属性诞生的使命,就是赐予我们没收浏览器这两个默认行为的权力。
三大核心形态解析
它主要有三个核心属性值:
1. auto(顺其自然,默认值)
一切照旧。内容滚到底部继续拉拉扯扯,就会理所当然地触发父容器的滚动链传递,并且该有橡皮筋特效的时候绝不含糊。
2. contain(锁住传导,保留特效)
这是解决滚动穿透的最佳利器!
只要在这个局部滚动的容器上加上 overscroll-behavior: contain;,它就像是被套上了一层隔绝结界:
当用户在这个容器里滚到底时,浏览器会阻断滚动链的向外传递(底下的页面绝对不会被连累),但也给用户留足了面子,依然保留诸如橡皮筋回弹的原生视觉反馈。
3. none(物理阉割,特效全无)
这招更绝。它不仅像 contain 一样阻断了向外传播的滚动链,还顺带着把这个容器边界上所有的默认原生动作(如回弹反馈、Android 边缘发光、浏览器的下拉刷新)拔了个精光。
当你要做一个完全依靠自己内部逻辑计算滑动的重交互网页应用(比如网页版地图规划工具,或者极其拟态原生的自定义组件)时,使用 none 可以彻底封杀掉系统级的干扰。
实战演练:告别坑爹体验
让我们通过两个可以直观交互的 Demo,来感受这股神秘的东方力量。
场景一:弹窗中的滚动穿透 (使用 contain 拦截)
这是极其多见的业务 Bug:当用户专注在弹窗内的协议条款狂拉到底时,身后的长列表竟然背着用户也拉到底了。加上 overscroll-behavior: contain 就能锁住这个不安分的小盒子。
场景二:禁用 PWA 网页/全屏 H5 的下拉刷新 (使用 none 阉割)
如果你正在写一个像模像样的 Web App 工具(比如移动版石墨文档),上面有一个精致的原生贴靠式的 Header 工具栏。 非常尴尬的是,只要用户随便在页面顶端手一划,整个屏幕就被类似微信浏览器的“网页由xxx提供”的深色底纹或者是浏览器的漏斗刷新圈给拽了下来,极其出戏。
此时你不需要找各种偏门的 JS 手势拦截库了,直接在最外围(如 body)挂上 none 即可。
高频面试题剖析
Q:在移动端或浮层组件上,如何处理滚动穿透的问题?
回答思路:
- 定性问题:明确滚动穿透是因为浏览器在局部滚动到达临界值后,默认启用了事件的冒泡连结机制拖动了下方的父容器。
- 吐槽旧时代:以前需要通过监听
.addEventListener('touchmove', preventDefault, {passive: false})或通过动态给body打上fixed和top偏移量进行暴力控制,维护成本极高,还容易丧失丝滑性。 - 主推当代王牌:甩出
overscroll-behavior。指出在滚动的弹层容器上直接设置overscroll-behavior: contain;即可完美截断传递,且不破坏自身的阻尼回弹 UI 体验。 - 展露场景厚度:进一步补充该属性还可以利用
none参数彻底阉割某些场景下烦人的浏览器原生下拉菜单或回弹刷新(比如打造沉浸式 Web Application 工具时非常有效)。明确表示这是纯 CSS 层面成本极低的防具。

