在移动端 H5 页面或部分 Web 组件的开发中,开发者们大概率遭遇过这样一个让人抓狂的场景: 页面中心弹出了一个小小的滚动弹窗(Modal),用户在弹窗里专心往下划。当弹窗内容到底了,如果手指还没离开屏幕继续往下扒拉——结果整个背后的页面内容开始跟着疯狂滚动!
这种因为局部滚动到头,导致把滚动事件“击穿”传递给下层父容器的现象,被称作滚动穿透(Scroll Chaining/Scroll Bleeding)。
在过去,为了制止这种“手贱”引起的联动反应,前端先驱们写出了无数篇名为《移动端彻底阻止弹窗滚动穿透》的文章。方案可谓惨烈:从通过 JS 监听 touchmove 并暴力 e.preventDefault(),到动态给 body 加 fixed 然后依靠 JS 算高度把页面死死钉住……不仅代码恶心,往往还会导致部分浏览器的原生弹性效果彻底报废。
直到 overscroll-behavior 带着圣光降临,几行 CSS 就优雅地平息了这一切。
我们要先理解,为什么浏览器非要触发这种“穿透”?
当我们用手指或者触控板滚动一个区域到达它的“边界(极值)”时,浏览器默认有两个连击动作:
overscroll-behavior 属性诞生的使命,就是赐予我们没收浏览器这两个默认行为的权力。
它主要有三个核心属性值:
auto(顺其自然,默认值)一切照旧。内容滚到底部继续拉拉扯扯,就会理所当然地触发父容器的滚动链传递,并且该有橡皮筋特效的时候绝不含糊。
contain(锁住传导,保留特效)这是解决滚动穿透的最佳利器!
只要在这个局部滚动的容器上加上 overscroll-behavior: contain;,它就像是被套上了一层隔绝结界:
当用户在这个容器里滚到底时,浏览器会阻断滚动链的向外传递(底下的页面绝对不会被连累),但也给用户留足了面子,依然保留诸如橡皮筋回弹的原生视觉反馈。
none(物理阉割,特效全无)这招更绝。它不仅像 contain 一样阻断了向外传播的滚动链,还顺带着把这个容器边界上所有的默认原生动作(如回弹反馈、Android 边缘发光、浏览器的下拉刷新)拔了个精光。
当你要做一个完全依靠自己内部逻辑计算滑动的重交互网页应用(比如网页版地图规划工具,或者极其拟态原生的自定义组件)时,使用 none 可以彻底封杀掉系统级的干扰。
让我们通过两个可以直观交互的 Demo,来感受这股神秘的东方力量。
这是极其多见的业务 Bug:当用户专注在弹窗内的协议条款狂拉到底时,身后的长列表竟然背着用户也拉到底了。加上 overscroll-behavior: contain 就能锁住这个不安分的小盒子。
如果你正在写一个像模像样的 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 层面成本极低的防具。