スクロールの制御 - 下にスワイプして更新やオーバーフロー効果のカスタマイズ

要約

CSS overscroll-behavior プロパティを使用すると、デベロッパーはコンテンツの上部または下部に到達したときに、ブラウザのデフォルトのオーバーフロー スクロール動作をオーバーライドできます。ユースケースとしては、モバイルで「下にスワイプして更新」機能を無効にする、オーバースクロール グローとラバーバンド効果を除去する、モーダル/オーバーレイの下にあるページ コンテンツのスクロールを防ぐことなどがあります。

背景

スクロール境界とスクロール チェーン

Chrome Android でのスクロール チェーン。

スクロールは、ページを操作するための最も基本的な方法の一つですが、ブラウザのデフォルトの動作が奇妙で、特定の UX パターンでは対処しにくい場合があります。たとえば、ユーザーによるスクロールが必要なアイテムが多数あるアプリドロワーがあるとします。一番下に達すると、消費するコンテンツがなくなるため、オーバーフロー コンテナはスクロールを停止します。つまり、ユーザーが「スクロール境界」に到達します。ただし、ユーザーがスクロールを続けるとどうなるかに注意してください。ドロワーの背後にあるコンテンツがスクロールを開始します。スクロールは親コンテナ(この例ではメインページ自体)に引き継がれています。

この動作はスクロール チェーンと呼ばれています。これは、コンテンツをスクロールする際のブラウザのデフォルトの動作です。デフォルトのほうが良い場合もありますが、望ましくない場合もあります。一部のアプリでは、ユーザーがスクロール境界に達したときに異なるユーザー エクスペリエンスを提供する必要があります。

下にスワイプして更新する効果

下にスワイプして更新は、Facebook や Twitter などのモバイルアプリで広く普及している直感的な操作です。ソーシャル フィードで画面を下にスワイプしてリリースすると、新しい投稿を読み込めるスペースが生まれます。実際に、この特定の UX は非常に人気を博しており、Android 版 Chrome のようなモバイル ブラウザでも同様の効果が起きています。ページの上部で下にスワイプすると、ページ全体が更新されます。

Twitter のカスタム「下にスワイプして更新」
(PWA でフィードの更新時)。
Chrome Android のネイティブな「下にスワイプして更新」操作
により、ページ全体が更新されます。

Twitter の PWA のような状況では、ネイティブの「下にスワイプして更新」アクションを無効にしたほうがよい場合があります。その理由は、このアプリでは、ユーザーが誤ってページを更新しないようにする必要があります。ダブル更新のアニメーションが表示される可能性もあります。または、ブラウザのアクションをカスタマイズして、サイトのブランディングに合わせて調整するほうがよい場合もあります。残念なことに、この種のカスタマイズを実現するのが難しいという問題があります。デベロッパーが不要な JavaScript を記述したり、非パッシブのタッチリスナーを追加してスクロールをブロックしたり、ページ全体を 100vw/vh の <div> に組み込んだりする(ページのオーバーフローを防ぐため)。この回避策は、スクロールのパフォーマンスに悪影響を及ぼす詳細ドキュメントです。

改善の余地あり!

overscroll-behavior のご紹介

overscroll-behavior プロパティは、コンテナをオーバースクロールした場合の動作(ページ自体も含む)を制御する CSS の新機能です。スクロール チェーンのキャンセル、プルして更新アクションの無効化とカスタマイズ、iOS でのラバーバンド効果の無効化(Safari に overscroll-behavior が実装されている場合)などに使用できます。何より素晴らしいのは、導入部で言及したハックのように、overscroll-behavior を使用してもページ パフォーマンスに悪影響が及ばないことです。

このプロパティには次の 3 つの値を指定できます。

  1. auto - デフォルトです。要素から発生したスクロールは、祖先要素に伝播される場合があります。
  2. contains - スクロール チェーンを防止します。スクロールは祖先に伝播されませんが、ノード内のローカル効果が表示されます。たとえば、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;
}

基本的には、チャットボックスのスクロール コンテキストとメインページを論理的に分離します。最終的に、ユーザーがチャット履歴の先頭または末尾に到達しても、メインページは表示されたままになります。チャットボックスで開始されたスクロールは伝播されません。

ページ オーバーレイのシナリオ

「アンダースクロール」シナリオのもう 1 つのバリエーションは、コンテンツが固定位置のオーバーレイの背後でスクロールする場合です。死者プレゼント overscroll-behavior が登場しました!ブラウザは利便性を意図していますが サイトにバグがあるように見えてしまいます

- overscroll-behavior: contain あり / なしのモーダル:

変更前: ページ コンテンツがオーバーレイの下にスクロールします。
変更後: ページ コンテンツがオーバーレイの下にスクロールされません。

下にスワイプして更新を無効にする

下にスワイプして更新の操作は、CSS で 1 行で無効にできます。ただし、ビューポートを定義する要素全体でスクロール チェーンがつながらないようにするだけです。ほとんどの場合、<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="refresher">
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
</div>

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

<script>
  let _startY;
  const inbox = document.querySelector('#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.classList.contains('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 を使用して、下にスワイプして更新するカスタム アニメーションを作成し、スクロールによるチャットボックス ウィジェットのエスケープを無効にしています。これにより、CSS overscroll-behavior なしでは得られなかったような最適なユーザー エクスペリエンスを実現できます。

デモを見る | ソース