本文へスキップ

無限スクロール

コンテナ内を下までスクロールすると自動でアイテムを追加読み込み。IntersectionObserverでsentinelを監視。

プレビュー

スクロール

操作枠内をスクロールすると動きます。

使いどころ

総件数が多い、または明確な終わりを設計時に決めにくいフィード(SNSタイムライン、通知一覧、記事一覧)に向く。ページ送りのクリックを挟まずに読み進められる没入感が利点で、このサンプルのようにIntersectionObserverでスクロール終端の少し手前から次のバッチを先読みすれば、ユーザーが本当の最下端に着く前に読み込みが完了し、途切れを感じさせない。向かないのは、フッターや関連リンクなど一覧の下にある固定コンテンツに到達してほしい画面、特定の位置への共有リンクやブラウザの戻るボタンでのスクロール位置復元が重要な画面、検索結果のように件数や順位を利用者が把握しておきたい画面である。後者はページ番号付きの一覧かページボタンの方が適し、無限スクロールは何件中何件目にいるかの感覚を利用者から奪いやすい。

仕組み

IntersectionObserverのrootにビューポートではなく.is-list(固定高さの内側スクロールコンテナ)自身を指定している点が要で、これによりページ全体ではなくカード内の小さな領域のスクロールでも監視が成立する。監視対象のsentinelは常にリストの最後尾に置かれ、新しい項目やスピナー、終端メッセージは全てinsertBefore(要素, sentinel)の形でsentinelの直前に挿入することで、sentinel自身の位置を最後尾に保ち続ける。rootMarginの下端20pxはsentinelが実際に見える20px手前で交差を検知させる先読みの仕掛けで、擬似的な800msの読み込み待ちがユーザーの体感の前に終わるよう調整されている。loadingという真偽値フラグで、読み込み中の重複発火(短時間に何度も交差イベントが飛ぶ)を防いでいる。プールを使い切ったらio.unobserve(sentinel)で監視自体を止め、以降の無駄な交差検知を防ぐ。

実装の注意

最も踏みやすいのは、ページ全体をスクロールさせる設計に転用するときにrootをlistのままにしてしまい、監視が発火しなくなる事故。ページ単位にするならrootをnull(ビューポート基準)に変え、sentinelもページの本当の末尾に置き直す必要がある。次に、コンテナの高さに対して初期コンテンツが少ないと、挿入直後にsentinelがすぐ画面内に入ってしまい、loadingフラグでガードしていても短時間に連続してaddItemsが呼ばれ、想定より速いペースで全件を読み込み切ってしまう暴走読み込みが起きやすい。初期表示だけでビューポートを満たせる件数を確保しておくのが対策になる。またrootMarginが小さすぎると、ユーザーが最下端に着いてからスピナーが表示され引っかかっているように感じ、大きすぎると本当に見る前のバッチまで先読みして帯域を無駄にする。適切な値はコンテンツの行の高さと想定スクロール速度から逆算して決めるべきである。

アクセシビリティ

無限スクロールの代表的な問題が「フッター到達不能問題」で、下に続くはずのサイトフッターや関連ナビが、読み込みが終わらない限り永遠に手前に来てしまい、キーボード・スクリーンリーダー利用者だけでなく晴眼のマウス利用者も到達できなくなる。このサンプルは.is-listという固定高さの内側コンテナだけをスクロールさせる構造で、ページのフッターはそもそもこのコンテナの外にあるため、この問題を実質的に回避できている。ただしページ全体スクロールに転用するときは同じ問題が再発するため、一定件数を自動読み込みしたら「もっと見る」ボタンへ切り替える対策が要る。加えて現状の実装には、新規追加された項目や読み込み中である旨をスクリーンリーダーに知らせるaria-live領域が無く、視覚的な「読み込み中…」の文字列は見えるユーザーにしか伝わらない。また.is-list自体にtabindexが無く、キーボード操作でこの内側コンテナを直接スクロールする手段が用意されていない(同じ辞典の仮想スクロールの.vs-viewportにはtabindex=0があり対照的)。

調整して使う

バッチサイズ(このサンプルは3件ずつ)は行の高さと読み込みコストで決める。細かい行なら5〜10件、重いカードなら1〜2件が扱いやすい。読み込み中の表現もスピナーに限らず、スケルトン行を数個差し込む形にすると、読み込み後の項目の形があらかじめ想像でき、切り替わりの唐突さが減る。rootMargin(先読み量)は接続速度が読めないモバイル中心の想定ならやや大きめに、PC中心で高速な想定なら小さめにする。チャットのように過去の履歴を上方向へ遡って読み込む逆方向の無限スクロールもあり、その場合はsentinelを先頭に置き、追加した項目ぶんscrollTopを補正してスクロール位置が飛ばないようにする実装が要る。件数に上限を設け、一定量を超えたら「もっと見る」ボタンに切り替えるハイブリッド型は、フッター到達不能問題への現実的な妥協案として使われる。

コード(コピーして使えます)

<div class="is-root">
  <div class="is-header">記事一覧</div>
  <div class="is-list" id="is-list">
    <div class="is-item"><span class="is-badge">新着</span>UI設計の基本原則</div>
    <div class="is-item">アニメーションで伝わるデザイン</div>
    <div class="is-item">タイポグラフィの選び方</div>
    <div class="is-item">カラーパレットの作り方</div>
    <div class="is-item">グリッドレイアウト入門</div>
    <div class="is-sentinel" id="is-sentinel"></div>
  </div>
</div>

タグ

同じカテゴリ「ナビ・リスト操作」のほかのパーツ

関連パーツ