データ表示
フィード
解説ありFeed
無限スクロールの記事一覧を、支援技術でも読み進められるようにする。
フィード(feed)とは?
フィードは、記事や投稿を縦に並べ、下までスクロールすると自動で続きが読み込まれるタイプの一覧 UI です。 SNS のタイムライン、ニュース一覧、ブログの記事ストリームなどでよく使われます。
普通のリストと違うのは、件数が動的に増えていくこと。 だからこそ「いま何件中の何番目を読んでいるのか」「いま読み込み中なのか」を、 目で見えない人にも伝える工夫が必要になります。
なぜアクセシビリティが大事なの?
無限スクロールは、作り方を間違えると次の人たちが取り残されます。
- スクリーンリーダーを使う人。記事が
<div>の羅列だと 「これは記事の一覧だ」「全○件の△番目だ」が伝わらず、現在地を見失います。 読み込み中かどうかも分からず、無音の待ち時間に不安になります。 - キーボードだけで操作する人。記事内のリンクを Tab で1つずつ辿るのは大変です。記事単位でジャンプできないと、長い一覧を読み進められません。
解決策は、コンテナを role="feed"、各記事を role="article" にして、aria-posinset / aria-setsize で位置と総数を、aria-busy で読み込み中を伝えること。 そして PageDown / PageUp で記事間を移動できるようにします。
ライブデモ(推奨実装)
下のフィードは APG に沿った実装です。記事にフォーカスを当て、マウスを使わずキーボードだけで記事を読み進めてみてください。
朝の散歩を習慣にした話
毎朝20分歩くだけで、頭がすっきりして一日の集中力が変わりました。続けるコツは「天気の良い日だけ」と決めないこと。
小さなキッチンの収納術
限られたスペースでも、縦の空間と扉裏を使えば収納量はぐっと増えます。よく使う道具ほど手前・腰の高さに。
読書記録をはじめてみた
読み終えた本の感想を3行だけ書く。たったそれだけで、後から内容を思い出しやすくなりました。
試してみよう:Tab で最初の記事にフォーカス → PageDown / PageUp で記事間を移動 → 「もっと読み込む」で続きが増え、フォーカスは新着の先頭へ移動します(迷子になりません)。
ポイント
スクリーンリーダーで記事にフォーカスすると、「記事, 朝の散歩を習慣にした話, 1 / 3」のように記事のタイトル・現在位置・総数が読み上げられます。 これが aria-labelledby と aria-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> をただ積み重ねただけの「見た目だけ同じ」フィードです。マウスではスクロールできますが、記事間ジャンプはできず、 支援技術には一覧の構造も現在地も読み込み状態も伝わりません。
朝の散歩を習慣にした話
毎朝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 パターンを踏めば「現在地・総数・読み込み状態」を きちんと伝えられます。スクロール検知で自動読み込みする場合も、「もっと読み込む」ボタンを併設しておくと、キーボード利用者にも確実です。
実装チェックリスト
- コンテナが
role="feed"で、aria-label等で名前が付いている - 各記事が
role="article"+tabindex="0"でフォーカスできる - 各記事に
aria-labelledbyで見出しが関連付いている aria-posinset/aria-setsizeが現在の位置・総数と常に一致している- 追加読み込み中は
aria-busy="true"、完了後にfalseへ戻る - PageDown / PageUp で記事間を移動できる(既定スクロールは
preventDefault) - 追加読み込み後もフォーカスを失わない(新着の先頭などへ移す)
- キーボードだけで読み進められ、フォーカスが見える(フォーカスリングを消していない)