データ表示

フィード

解説あり

Feed

無限スクロールの記事一覧を、支援技術でも読み進められるようにする。

フィード(feed)とは?

フィードは、記事や投稿を縦に並べ、下までスクロールすると自動で続きが読み込まれるタイプの一覧 UI です。 SNS のタイムライン、ニュース一覧、ブログの記事ストリームなどでよく使われます。

普通のリストと違うのは、件数が動的に増えていくこと。 だからこそ「いま何件中の何番目を読んでいるのか」「いま読み込み中なのか」を、 目で見えない人にも伝える工夫が必要になります。

なぜアクセシビリティが大事なの?

無限スクロールは、作り方を間違えると次の人たちが取り残されます。

解決策は、コンテナを role="feed"、各記事を role="article" にして、aria-posinset / aria-setsize で位置と総数を、aria-busy で読み込み中を伝えること。 そして PageDown / PageUp で記事間を移動できるようにします。

ライブデモ(推奨実装)

下のフィードは APG に沿った実装です。記事にフォーカスを当て、マウスを使わずキーボードだけで記事を読み進めてみてください。

アクセシブルなフィード

朝の散歩を習慣にした話

毎朝20分歩くだけで、頭がすっきりして一日の集中力が変わりました。続けるコツは「天気の良い日だけ」と決めないこと。

小さなキッチンの収納術

限られたスペースでも、縦の空間と扉裏を使えば収納量はぐっと増えます。よく使う道具ほど手前・腰の高さに。

読書記録をはじめてみた

読み終えた本の感想を3行だけ書く。たったそれだけで、後から内容を思い出しやすくなりました。

試してみよう:Tab で最初の記事にフォーカス → PageDown / PageUp で記事間を移動 → 「もっと読み込む」で続きが増え、フォーカスは新着の先頭へ移動します(迷子になりません)。

ポイント

スクリーンリーダーで記事にフォーカスすると、「記事, 朝の散歩を習慣にした話, 1 / 3」のように記事のタイトル・現在位置・総数が読み上げられます。 これが aria-labelledbyaria-posinset / aria-setsize の効果です。

キーボード操作

キー動作必須/任意
PageDown次の記事へフォーカス移動必須
PageUp前の記事へフォーカス移動必須
Tab記事内のリンクなど、次のフォーカス可能要素へ移動必須
Home / End最初 / 最後の記事へフォーカス移動任意(推奨)

補足

記事間の移動に PageDown / PageUp を使うのは、Tab を記事内のリンク移動のために空けておくためです。 ハンドラ内では preventDefault() でブラウザ既定のスクロールを止めます。

必要な WAI-ARIA / ロール

付ける場所属性 / ロール意味
コンテナrole="feed"動的に増えるスクロール可能な記事一覧であることを示す。
コンテナaria-busy="true | false"追加読み込み中は true読み込み完了後は必ず false に戻す
コンテナaria-label / aria-labelledbyフィード自体に名前を付ける(「新着記事」など)。
各記事role="article" + tabindex="0"1件の記事として認識させ、フォーカス可能にする。
各記事aria-labelledby="タイトルのid"記事の名前(見出し)を関連付ける。フォーカス時に読み上げられる。
各記事aria-posinset + aria-setsize「全○件中の△番目」を示す。件数が増えたら全記事で更新する。

実装:推奨パターン(Good)

良い例 / 推奨

role="feed" > role="article" の構造にし、 位置・総数・読み込み状態を ARIA で同期させます。

マークアップ:

<div role="feed" aria-busy="false" aria-label="新着記事">
  <article
    role="article"
    tabindex="0"
    aria-labelledby="post-1-title"
    aria-posinset="1"
    aria-setsize="3">
    <h3 id="post-1-title">朝の散歩を習慣にした話</h3>
    <p>毎朝20分歩くだけで、頭がすっきりして……</p>
  </article>

  <article
    role="article"
    tabindex="0"
    aria-labelledby="post-2-title"
    aria-posinset="2"
    aria-setsize="3">
    <h3 id="post-2-title">小さなキッチンの収納術</h3>
    <p>限られたスペースでも、工夫しだいで……</p>
  </article>

  <!-- …以下、記事が続く -->
</div>

<button type="button" id="load-more">もっと読み込む</button>

記事間の移動と追加読み込み(位置・件数・aria-busy・フォーカスの管理がすべて):

const feed = document.getElementById('feed-region');
const loadBtn = document.getElementById('feed-load');

// 記事と記事の間をキーボードで移動する(PageDown / PageUp)
function getArticles() {
  return Array.from(feed.querySelectorAll('[role="article"]'));
}

feed.addEventListener('keydown', (e) => {
  const articles = getArticles();
  const current = document.activeElement?.closest('[role="article"]');
  const index = articles.indexOf(current);
  if (index === -1) return;

  let target = null;
  if (e.key === 'PageDown') target = articles[index + 1];
  else if (e.key === 'PageUp') target = articles[index - 1];
  else if (e.key === 'Home') target = articles[0];
  else if (e.key === 'End') target = articles[articles.length - 1];

  if (target) {
    e.preventDefault();      // 既定のスクロールを止める
    target.focus();
  }
});

// 追加読み込み:件数(setsize)・位置(posinset)を更新し、
// 読み込み中は aria-busy="true"。フォーカスは最初の新着記事へ移す。
loadBtn.addEventListener('click', () => {
  feed.setAttribute('aria-busy', 'true');   // ← 読み込み中を支援技術へ通知
  setTimeout(() => {
    const total = getArticles().length + 2;
    // …新しい <article> を生成し posinset / setsize を付けて append…
    getArticles().forEach((a, i) => {
      a.setAttribute('aria-posinset', String(i + 1));
      a.setAttribute('aria-setsize', String(total));
    });
    feed.setAttribute('aria-busy', 'false');
    firstNewArticle.focus();   // ← フォーカスを失わせない
  }, 600);
});

補足

追加読み込みのときは 新しく追加した記事の先頭にフォーカスを移すのが親切です。 何もしないと、DOM が変わった瞬間にフォーカスが文書の先頭へ飛び、現在地を見失います。 あわせて aria-setsize全記事で新しい総数に更新してください。

アンチパターン(Bad)

下は <div> をただ積み重ねただけの「見た目だけ同じ」フィードです。マウスではスクロールできますが、記事間ジャンプはできず、 支援技術には一覧の構造も現在地も読み込み状態も伝わりません。

div の羅列で作った壊れたフィード

朝の散歩を習慣にした話

毎朝20分歩くだけで、頭がすっきりして一日の集中力が変わりました。

小さなキッチンの収納術

限られたスペースでも、縦の空間と扉裏を使えば収納量はぐっと増えます。

読書記録をはじめてみた

読み終えた本の感想を3行だけ書く。たったそれだけで思い出しやすくなりました。

もっと読み込む

試してみよう:記事にフォーカスできず PageDown でも移動しません。「もっと読み込む」を押すと記事は増えますが、フォーカスは迷子になり、何件中の何番目かも分かりません。

<!-- ❌ アンチパターン -->
<!-- ただの div の積み重ね。feed/article のロールも位置・件数も無い -->
<div class="list">
  <div class="card">
    <h3>朝の散歩を習慣にした話</h3>
    <p>毎朝20分歩くだけで……</p>
  </div>
  <div class="card">
    <h3>小さなキッチンの収納術</h3>
    <p>限られたスペースでも……</p>
  </div>
</div>

<div class="more" onclick="loadMore()">もっと読み込む</div>

悪い例 / 避ける

この実装の問題点:

  • 一覧の構造が伝わらないrole="feed" / role="article" が無く、ただのテキストの塊に聞こえる。
  • 現在地が分からないaria-posinset / aria-setsize が無く、「全何件の何番目か」を案内できない。
  • 読み込み中が伝わらないaria-busy が無く、追加読み込みが起きても支援技術には無言のまま。
  • 記事間を移動できない — フォーカスできず、PageDown / PageUp での記事ジャンプも不可。
  • フォーカスが迷子になる — 追加読み込み後のフォーカス管理が無く、現在地を見失う。

ポイント

無限スクロールでも、APG の feed パターンを踏めば「現在地・総数・読み込み状態」を きちんと伝えられます。スクロール検知で自動読み込みする場合も、「もっと読み込む」ボタンを併設しておくと、キーボード利用者にも確実です。

実装チェックリスト


原文(英語):Feed Pattern — W3C APG(新しいタブで開きます)