掌控滚动操作 - 自定义下拉刷新和溢出效果

要点

CSS overscroll-behavior 属性允许开发者在到达内容顶部/底部时替换浏览器的默认溢出滚动行为。用例包括在移动设备上停用下拉刷新功能、移除滚动回弹发光和橡皮筋效果,以及防止页面内容在模态/叠加层下方滚动。

背景

滚动边界和滚动链

Android 版 Chrome 上的滚动链接。

滚动是与网页互动最基本的方式之一,但由于浏览器古怪的默认行为,某些用户体验模式可能很难处理。例如,假设有一个应用抽屉,其中包含大量用户可能需要滚动浏览的内容。当用户到达底部时,溢出容器会停止滚动,因为没有更多内容可供浏览。 换句话说,用户到达了“滚动边界”。但请注意,如果用户继续滚动,会发生什么情况。抽屉式导航栏后面的内容开始滚动!滚动操作由父级容器(示例中的主页面本身)接管。

事实证明,这种行为称为滚动链;滚动内容时浏览器的默认行为。通常,默认设置非常不错,但有时可能不符合预期,甚至出乎意料。某些应用可能希望在用户到达滚动边界时提供不同的用户体验。

下拉刷新效果

下拉刷新是一种直观的手势,由 Facebook 和 Twitter 等移动应用推广。在社交动态中下拉并松开,可为加载更多近期帖子腾出新空间。事实上,这种特定的用户体验已经非常流行,以至于 Android 上的 Chrome 等移动浏览器也采用了相同的效果。在页面顶部向下滑动会刷新整个页面:

Twitter 在其 PWA 中刷新信息流时
使用的自定义下拉刷新。
Android 版 Chrome 的原生下拉刷新操作
会刷新整个页面。

对于 Twitter PWA 等情况,停用原生下拉刷新操作可能更合理。为什么呢?在此应用中,您可能不希望用户意外刷新页面。还可能会看到双重刷新动画!或者,最好自定义浏览器的操作,使其更贴合网站的品牌。遗憾的是,这种自定义很难实现。开发者最终会编写不必要的 JavaScript、添加非被动触摸监听器(会阻止滚动),或将整个页面粘贴到 100vw/vh <div> 中(以防止页面溢出)。这些权宜解决方法对滚动性能有详细记录的负面影响。

我们能做得更好!

隆重推出 overscroll-behavior

overscroll-behavior 属性是一项新的 CSS 功能,用于控制在您过度滚动容器(包括页面本身)时会发生什么。您可以使用它来取消滚动链接、停用/自定义下拉刷新操作、停用 iOS 上的橡皮筋效果(当 Safari 实现 overscroll-behavior 时)等。 最棒的是,使用 overscroll-behavior 不会对网页性能产生不利影响,不像简介中提到的那些技巧!

该属性有三个可能的值:

  1. auto - 默认值。源自该元素的滚动可能会传播到祖先元素。
  2. contain - 防止滚动链。滚动不会传播到祖先,但会显示节点内的局部效果。例如,Android 上的滚动回弹发光效果或 iOS 上的橡皮筋效果,可在用户到达滚动边界时通知用户。注意:在 html 元素上使用 overscroll-behavior: contain 可防止滚动回弹导航操作。
  3. none - 与 contain 相同,但它还会阻止节点本身内的滚动回弹效果(例如 Android 滚动回弹发光或 iOS 橡皮筋效果)。

我们来看一些示例,了解如何使用 overscroll-behavior。

防止滚动操作逃逸出固定位置元素

对话框场景

聊天窗口下方的内容也会滚动 :(

假设有一个固定在页面底部的聊天框。其目的是让聊天框成为一个独立组件,并与后面的内容分开滚动。不过,由于滚动链,用户一到达聊天记录中的最后一条消息,文档就会开始滚动。

对于此应用,更合适的做法是让源自聊天框内的滚动操作停留在聊天中。为此,我们可以将 overscroll-behavior: contain 添加到包含聊天消息的元素中:

#chat .msgs {
  overflow: auto;
  overscroll-behavior: contain;
  height: 300px;
}

本质上,我们是在聊天框的滚动上下文和主页面之间创建逻辑分隔。最终结果是,当用户到达聊天记录的顶部/底部时,主页面会保持不动。在聊天框中开始的滚动不会向外传播。

页面叠加场景

“下滚”场景的另一种变体是,您会看到内容在固定位置叠加层后面滚动。是时候来一场 overscroll-behavior大赠送了!浏览器本想提供帮助,但最终却让网站看起来存在 bug。

示例 - 包含和不包含 overscroll-behavior: contain 的模态:

之前:网页内容在叠加层下方滚动。
之后:网页内容不会在叠加层下方滚动。

停用下拉刷新

只需一行 CSS 代码即可关闭下拉刷新操作。只需在整个视口定义元素上阻止滚动链接即可。在大多数情况下,该值为 <html> 或 <body>:

body {
  /* Disables pull-to-refresh but allows overscroll glow effects. */
  overscroll-behavior-y: contain;
}

通过这项简单的添加,我们修复了聊天框演示中的双重下拉刷新动画,并可以改为实现使用更简洁的加载动画的自定义效果。收件箱刷新时,整个收件箱也会模糊:

之前
之后

以下是完整代码的代码段:

<style>
  body.refreshing #inbox {
    filter: blur(1px);
    touch-action: none; /* prevent scrolling */
  }
  body.refreshing .refresher {
    transform: translate3d(0,150%,0) scale(1);
    z-index: 1;
  }
  .refresher {
    --refresh-width: 55px;
    pointer-events: none;
    width: var(--refresh-width);
    height: var(--refresh-width);
    border-radius: 50%;
    position: absolute;
    transition: all 300ms cubic-bezier(0,0,0.2,1);
    will-change: transform, opacity;
    ...
  }
</style>

<div class="refre>she<r"
  div class=&qu><ot;l>oad<ing-bar"/div
  div>< cla>ss=<"loading-bar"></div>
  <div class="loading><-bar>&<quot>;/<div
  div class=&q><uot;loading><-bar&quo>t;</div
/>div

section id="inbox"!-- msgs --/section

script
  let _startY;
  const inbox = document.querySel>ector('#inbox');

  inbox.addEventListener('touchstart', e = {
    _startY = e.touches>[0].pageY;
  }, {passive: true});

  inbox.addEventListener('touchmove', e = {
    const y = e.touches[0].pageY;
    // Activate custom pull-to-refresh effects when at the top of the container&&
  >  // and &&user is scrolling up.
    if (document.scrollingElement.scrollTop === 0  y  _startY 
        !document.body.class<List.co>ntains('refreshing')) {
      // refresh inbox.
    }
  }, {passive: true});
/script

停用滚动回弹发光和橡皮筋效果

如需在到达滚动边界时停用回弹效果,请使用 overscroll-behavior-y: none:

body {
  /* Disables pull-to-refresh and overscroll glow effect.
     Still keeps swipe navigations. */
  overscroll-behavior-y: none;
}
之前:到达滚动边界时会显示光晕。
之后:停用发光效果。

完整演示

综合以上内容,完整的聊天框演示使用 overscroll-behavior 创建自定义下拉刷新动画,并禁止滚动操作超出聊天框 widget 的范围。这提供了最佳的用户体验,如果没有 CSS overscroll-behavior,很难实现。

查看演示 | 源代码