無限スクロールとは

一覧の下までスクロールすると、自動的に続きの項目が読み込まれて表示される仕組み。

概要

記事一覧や SNS のタイムラインのように、下までスクロールすると自動的に続きの項目が読み込まれて表示される仕組みです。 ページを切り替える操作が要らないため、読者はスクロールを続けるだけで次々にコンテンツを見られます。一覧の最後に近づいたことを検知して読み込む実装には、IntersectionObserver がよく使われます。

別名

表記ゆれ:無限スクロール、インフィニットスクロール、もっと見るボタン(ロードモア)、infinite scroll

「もっと見る」ボタンを押したときだけ次の項目を読み込む方式は、自動で読み込む無限スクロールとは厳密には別物ですが、 どちらも「ページ番号を切り替えない一覧の読み込み方式」として同じ文脈で語られることが多い言葉です。

AI への伝え方

「無限スクロールにして」で通じます。自動で読み込むか「もっと見る」ボタンを挟むか、何件読み込んだら止めるか(上限)を伝えると仕様がぶれません。 フッターまでスクロールしてほしい場合は無限スクロールと相性が悪いため、フッターの扱いも合わせて伝えると安心です。 IntersectionObserver は番兵要素が画面に入った「瞬間」しか発火しないため、1 回の読み込みで番兵が画面内に留まったままだと、それ以降は二度と発火しません。読み込みのたびに監視を外して登録し直す(再度 observe する)実装にしてもらうと、この詰まりを避けられます。

似たパーツとの違い

「ページネーション」はページという単位に区切ってリンクで移動するのに対し、無限スクロールはページの概念自体がなく、スクロールに合わせて自動的に続きが読み込まれる方式です。 ページネーションは特定のページへ直接戻りやすく、無限スクロールは読み進めやすいという、それぞれ異なる利点があります。

デモ

コード

HTML
<div class="infinite-scroll-demo">
  <p class="infinite-scroll-demo__lead">下にスクロールすると、一覧の続きが自動的に読み込まれます(最大 30 件)。</p>
  <ul class="infinite-scroll-demo__list" id="infinite-scroll-demo-list"></ul>
  <p class="infinite-scroll-demo__sentinel" id="infinite-scroll-demo-sentinel">読み込み中…</p>
  <p class="infinite-scroll-demo__end" id="infinite-scroll-demo-end" hidden>これ以上の項目はありません。</p>
</div>
CSS
.infinite-scroll-demo {
  font-family: sans-serif;
  color: #333333;
}

.infinite-scroll-demo__lead {
  margin: 0 0 16px;
  line-height: 1.7;
}

.infinite-scroll-demo__list {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.infinite-scroll-demo__list li {
  padding: 12px 14px;
  background: #ffffff;
  border: 1px solid #e6e6e6;
  border-radius: 8px;
  font-size: 14px;
}

.infinite-scroll-demo__sentinel {
  margin: 0 0 40px;
  padding: 10px 0;
  text-align: center;
  font-size: 13px;
  color: #666666;
}

.infinite-scroll-demo__end {
  margin: 0 0 40px;
  padding: 10px 0;
  text-align: center;
  font-size: 13px;
  color: #666666;
}

/* hidden 属性の要素に display を指定していないため、打ち消しは不要 */
JS
const list = document.getElementById("infinite-scroll-demo-list");
const sentinel = document.getElementById("infinite-scroll-demo-sentinel");
const endMessage = document.getElementById("infinite-scroll-demo-end");

const PAGE_SIZE = 5;
const MAX_ITEMS = 30;
let count = 0;

function loadMore() {
  // 「読み込み中…」が一瞬でも見えるよう、少し待ってから追加する
  setTimeout(() => {
    const remaining = MAX_ITEMS - count;
    const nextCount = Math.min(PAGE_SIZE, remaining);

    for (let i = 0; i < nextCount; i += 1) {
      count += 1;
      const li = document.createElement("li");
      li.textContent = `項目 ${count}`;
      list.appendChild(li);
    }

    if (count >= MAX_ITEMS) {
      sentinel.hidden = true;
      endMessage.hidden = false;
      observer.disconnect();
      return;
    }

    // 番兵がデモ枠に収まりきっていて交差状態が変化しないままだと、
    // IntersectionObserver は一度発火したきり二度と呼ばれない。
    // 監視をいったん外して登録し直し、まだ見えているなら再度発火させる
    observer.unobserve(sentinel);
    observer.observe(sentinel);
  }, 300);
}

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) loadMore();
  });
});

observer.observe(sentinel);