Webパーツ辞典

無限スクロールInfinite scroll

一覧のいちばん下までスクロールすると、次の件数を自動で読み込んで継ぎ足していく仕組み。押して継ぎ足す「もっと見る」ボタン方式もある。

表示例

別名・英語表記

  • 無限ロード
  • エンドレススクロール
  • 自動読み込み
  • インフィニットスクロール
  • スクロールで追加読み込み
  • Infinite scroll

実装例

<div class="x-infinite-scroll">
  <ul class="x-infinite-scroll__list" aria-label="新着記事">
    <li><a class="x-infinite-scroll__item" href="/posts/101/">朝5分でできる作り置き</a></li>
    <li><a class="x-infinite-scroll__item" href="/posts/102/">冬の電気代を下げる設定</a></li>
    <li><a class="x-infinite-scroll__item" href="/posts/103/">はじめての家計簿アプリ</a></li>
  </ul>
  <!-- 一覧の末尾に置く目印。これが画面に入ったら次の10件を読み込む -->
  <div class="x-infinite-scroll__sentinel" role="status">
    <span class="x-infinite-scroll__spin" aria-hidden="true"></span>
    <span>続きを読み込み中</span>
  </div>
</div>

AIへの指示文

「無限スクロール(Infinite scroll)」を実装してください。

## これは何か
一覧のいちばん下までスクロールすると、次の件数を自動で読み込んで継ぎ足していく仕組み。押して継ぎ足す「もっと見る」ボタン方式もある。

## 使いどころ
- SNS のタイムラインや画像の一覧のように、順に眺めること自体が目的で、特定の1件を探し直すことが少ないとき
- スマートフォンで、ページ送りのボタンを押させる手間を減らしたいとき

## 実装の要件
- 一覧の末尾に目印の要素を置き、IntersectionObserver で画面に入ったら次を読み込む。scroll イベントで位置を計算しない
- rootMargin で画面の少し手前(例: 200px)から読み込み始めると、待ち時間が見えにくい
- 読み込み中は二重に要求を出さないよう止める。最後まで読んだら監視をやめ、「すべて表示しました」と出す
- 読み込みに失敗したら自動の再試行を繰り返さず、「再読み込み」ボタンを出す
- フッターに辿り着けなくなる。読み込むたびにフッターが下へ逃げるので、フッターに会社情報や問い合わせを置くサイトでは、数回読み込んだら「もっと見る」ボタンに切り替える
- 詳細を開いてから戻ると、読み込んだ分が消えて先頭に戻されることがある。読み込んだページ番号を history.replaceState で URL に残し、戻ったときに復元する
- 検索エンジンのクローラーはスクロールしないので、継ぎ足した分は見つけてもらえない。?page=2 のような個別の URL でも同じ一覧を出せるようにしておく
- 件数が増えるほど DOM が膨らんで重くなる。数百件を超えるなら、画面外の要素を間引く(仮想スクロール)
- 目的の1件を探す一覧(検索結果・商品の比較)には向かない。何ページ目にあったかを覚えられるページネーションを使う

## アクセシビリティの要件
- キーボードでは一覧の後ろの要素へ Tab で抜けられなくなる。一覧の前に「一覧をとばす」リンクを置くか、「もっと見る」ボタン方式にする
- 読み込み中と読み込みの完了を role="status" の領域で知らせる(「10件を追加しました」など)。フォーカスは動かさない
- 記事が並ぶ一覧なら、WAI-ARIA APG の Feed パターン(role="feed" と article、aria-busy、aria-setsize / aria-posinset)を使うと、読み上げ環境で記事単位に移動できる。Page Down で次の記事へ移るキー操作も実装する

## 参考にする実装
この形を出発点にして、プロジェクトの命名規則・色・余白に合わせて調整してください。

```html
<div class="x-infinite-scroll">
  <ul class="x-infinite-scroll__list" aria-label="新着記事">
    <li><a class="x-infinite-scroll__item" href="/posts/101/">朝5分でできる作り置き</a></li>
    <li><a class="x-infinite-scroll__item" href="/posts/102/">冬の電気代を下げる設定</a></li>
    <li><a class="x-infinite-scroll__item" href="/posts/103/">はじめての家計簿アプリ</a></li>
  </ul>
  <!-- 一覧の末尾に置く目印。これが画面に入ったら次の10件を読み込む -->
  <div class="x-infinite-scroll__sentinel" role="status">
    <span class="x-infinite-scroll__spin" aria-hidden="true"></span>
    <span>続きを読み込み中</span>
  </div>
</div>
```

```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-infinite-scroll,
.x-infinite-scroll *:not([class*="fa-"]),
.x-infinite-scroll *:not([class*="fa-"])::before,
.x-infinite-scroll *:not([class*="fa-"])::after {
  box-sizing: border-box;
  /* 貼った先の既定の書体に引きずられないようにする。書体が変わると
     文字の幅と高さが変わり、枠のサイズまでずれる */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
}

.x-infinite-scroll {
  width: 280px;
  border: 1px solid #e3e6ea;
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
}
.x-infinite-scroll__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.x-infinite-scroll__list > li + li {
  border-top: 1px solid #e3e6ea;
}
.x-infinite-scroll__item {
  display: block;
  padding: 12px 14px;
  color: #1b2028;
  font-size: 13px;
  text-decoration: none;
}
.x-infinite-scroll__item:hover {
  color: #1d4ed8;
  text-decoration: underline;
}
.x-infinite-scroll__item:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: -2px;
}
.x-infinite-scroll__sentinel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 14px;
  border-top: 1px solid #e3e6ea;
  background: #f6f7f9;
  color: #606a7b;
  font-size: 12px;
}
.x-infinite-scroll__spin {
  width: 16px;
  height: 16px;
  border: 2px solid #cfd4dc;
  border-top-color: #1d4ed8;
  border-radius: 50%;
  animation: x-infinite-scroll-turn 0.8s linear infinite;
}
@keyframes x-infinite-scroll-turn {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .x-infinite-scroll__spin {
    animation: none;
  }
}
```

## 出力の約束
- HTML と CSS を分けて出す。JavaScript が要るときは素の JavaScript で書き、UIフレームワークは使わない。
- 参考実装がアイコンに Font Awesome のクラスを使っている場合は、プロジェクトで使っているアイコンかインライン SVG に置き換えてよい。
- 上のアクセシビリティ要件を満たす。満たせない箇所があるときは、理由を添えて先に知らせる。
- 色・角丸・余白は既存のデザイントークンがあればそれを使う。無ければ CSS 変数として切り出す。

使いどころ

  • SNS のタイムラインや画像の一覧のように、順に眺めること自体が目的で、特定の1件を探し直すことが少ないとき
  • スマートフォンで、ページ送りのボタンを押させる手間を減らしたいとき

注意点

  • フッターに辿り着けなくなる。読み込むたびにフッターが下へ逃げるので、フッターに会社情報や問い合わせを置くサイトでは、数回読み込んだら「もっと見る」ボタンに切り替える
  • 詳細を開いてから戻ると、読み込んだ分が消えて先頭に戻されることがある。読み込んだページ番号を history.replaceState で URL に残し、戻ったときに復元する
  • 検索エンジンのクローラーはスクロールしないので、継ぎ足した分は見つけてもらえない。?page=2 のような個別の URL でも同じ一覧を出せるようにしておく
  • 件数が増えるほど DOM が膨らんで重くなる。数百件を超えるなら、画面外の要素を間引く(仮想スクロール)
  • 目的の1件を探す一覧(検索結果・商品の比較)には向かない。何ページ目にあったかを覚えられるページネーションを使う

アクセシビリティの要点

  • キーボードでは一覧の後ろの要素へ Tab で抜けられなくなる。一覧の前に「一覧をとばす」リンクを置くか、「もっと見る」ボタン方式にする
  • 読み込み中と読み込みの完了を role="status" の領域で知らせる(「10件を追加しました」など)。フォーカスは動かさない
  • 記事が並ぶ一覧なら、WAI-ARIA APG の Feed パターン(role="feed" と article、aria-busy、aria-setsize / aria-posinset)を使うと、読み上げ環境で記事単位に移動できる。Page Down で次の記事へ移るキー操作も実装する

見分け方

関連するパーツ