Data Display

Feed

Available

Makes an infinitely scrolling article list navigable and readable with assistive technologies.

What is a Feed?

A feed is a list UI that displays articles or posts vertically,automatically loading more content as the user scrolls to the bottom. It is commonly used for social media timelines, news lists, and blog article streams.

What sets a feed apart from a regular list is that the number of items grows dynamically. This is precisely why it's important to convey "which item out of how many the user is currently reading" and "whether new content is currently loading" — even to people who cannot see the screen.

Why Does Accessibility Matter?

Infinite scroll, when implemented incorrectly, leaves these people behind:

The solution is to mark the container as role="feed" and each article as role="article", use aria-posinset / aria-setsize for position and total count, and aria-busy for loading state. Then enable PageDown / PageUp navigation between articles.

Live Demo (Recommended Implementation)

The feed below follows the APG pattern. Focus on an article and try navigating through the list without using a mouse — keyboard only.

Accessible Feed

Making Morning Walks a Habit

Just 20 minutes of walking each morning clears my head and improves my focus for the whole day. The trick is not limiting yourself to only walking on nice days.

Storage Tips for a Small Kitchen

Even with limited space, making use of vertical areas and the back of doors can dramatically increase your storage capacity. Keep frequently used items at arm's reach.

Starting a Reading Journal

Writing just three lines about each book after finishing it — that small habit alone makes it so much easier to recall what I read later on.

Try it: Tab to the first article → PageDown / PageUp to move between articles → Ctrl+End to move outside the feed → Click 'Load more' to add articles — focus moves to the first new article (you won't get lost).

Tip

When a screen reader focuses on an article, it announces something like "article, Making Morning Walks a Habit, 1 of 3" — conveying the article title, current position, and total count. This is the effect of aria-labelledby combined with aria-posinset / aria-setsize.

Keyboard Interaction

KeyActionPriority
PageDownMove focus to the next articleRecommended
PageUpMove focus to the previous articleRecommended
TabMove to the next focusable element within the article, such as a linkRequired
Ctrl + Home / Ctrl + EndMove to the first focusable element before / after the feed (exit the feed)Recommended

Note

PageDown / PageUp is used for moving between articles so thatTab remains available for navigating links within an article. The handler calls preventDefault() to suppress the browser's default scroll behavior.

Required WAI-ARIA Roles and Properties

TargetAttribute / RoleMeaning
Containerrole="feed"Indicates a dynamically growing, scrollable list of articles.
Containeraria-busy="true | false"Set to true during loading. Must be set back to false after loading completes.
Containeraria-label / aria-labelledbyProvides an accessible name for the feed (e.g., "Latest articles").
Each articlerole="article" + tabindex="0"Identifies each item as an article and makes it focusable.
Each articlearia-labelledby="title-id"Associates the article with its heading. Read aloud when the article receives focus.
Each articlearia-posinset + aria-setsizeIndicates "item N of M." Must be updated across all articles when the total count changes.

Implementation: Recommended Pattern (Good)

Good / Recommended

Use a role="feed" > role="article" structure and keep position, count, and loading state in sync via ARIA attributes.

Markup:

<div role="feed" aria-busy="false" aria-label="Latest articles">
  <article
    role="article"
    tabindex="0"
    aria-labelledby="post-1-title"
    aria-posinset="1"
    aria-setsize="3">
    <h3 id="post-1-title">Making Morning Walks a Habit</h3>
    <p>Just 20 minutes of walking each morning clears my head and…</p>
  </article>

  <article
    role="article"
    tabindex="0"
    aria-labelledby="post-2-title"
    aria-posinset="2"
    aria-setsize="3">
    <h3 id="post-2-title">Storage Tips for a Small Kitchen</h3>
    <p>Even with limited space, a little creativity goes a long way…</p>
  </article>

  <!-- …more articles follow -->
</div>

<button type="button" id="load-more">Load more</button>

Navigation between articles and loading more (managing position, count, aria-busy, and focus):

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

// Navigate between articles (PageDown / PageUp / Ctrl+Home / Ctrl+End)
function getArticles() {
  return Array.from(feed.querySelectorAll('[role="article"]'));
}

feed.addEventListener('keydown', (e) => {
  // Ctrl+Home / Ctrl+End: move focus outside the feed
  if (e.ctrlKey && e.key === 'Home') {
    e.preventDefault();
    feed.previousElementSibling?.focus?.();
    return;
  }
  if (e.ctrlKey && e.key === 'End') {
    e.preventDefault();
    loadBtn.focus();
    return;
  }

  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];

  if (target) {
    e.preventDefault();      // Prevent default scroll behavior
    target.focus();
  }
});

// Load more: update setsize/posinset, set aria-busy="true" during load,
// then move focus to the first newly added article.
loadBtn.addEventListener('click', () => {
  feed.setAttribute('aria-busy', 'true');   // ← Notify assistive tech of loading state
  setTimeout(() => {
    const total = getArticles().length + 2;
    // …generate new <article> elements with posinset / setsize and 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();   // ← Don't let focus get lost
  }, 600);
});

Note

When loading more content, it's best practice to move focus to the first newly added article. Without this, focus jumps to the top of the document when the DOM changes, causing the user to lose their place. Also make sure to update aria-setsize on all articles with the new total count.

Anti-pattern (Bad)

Below is a feed built by simply stacking <div> elements — it only looks the same.You can scroll with a mouse, but there's no way to jump between articles, and assistive technologies receive no information about the list structure, current position, or loading state.

A broken feed made of stacked divs

Making Morning Walks a Habit

Just 20 minutes of walking each morning clears my head and improves my focus for the whole day.

Storage Tips for a Small Kitchen

Even with limited space, making use of vertical areas and the back of doors can dramatically increase your storage capacity.

Starting a Reading Journal

Writing just three lines about each book after finishing it — that small habit alone makes it so much easier to recall.

Load more

Try it: Articles can't receive focus and PageDown doesn't move between them. Clicking 'Load more' adds articles, but focus gets lost and there's no indication of position or count.

<!-- ❌ Anti-pattern -->
<!-- Just stacked divs. No feed/article roles, no position or count info -->
<div class="list">
  <div class="card">
    <h3>Making Morning Walks a Habit</h3>
    <p>Just 20 minutes of walking each morning…</p>
  </div>
  <div class="card">
    <h3>Storage Tips for a Small Kitchen</h3>
    <p>Even with limited space…</p>
  </div>
</div>

<div class="more" onclick="loadMore()">Load more</div>

Bad / Avoid

Problems with this implementation:

  • List structure is not conveyed — Without role="feed" / role="article", it sounds like an unstructured block of text.
  • Position is unknown — Without aria-posinset / aria-setsize, there's no way to indicate "item X of Y."
  • Loading state is not communicated — Without aria-busy, assistive technologies remain silent when new content loads.
  • Can't navigate between articles — Articles can't receive focus, and PageDown / PageUp article jumping doesn't work.
  • Focus gets lost — No focus management after loading more content, causing users to lose their place.

Tip

Even with infinite scroll, following the APG feed pattern lets you properly communicate "current position, total count, and loading state." When using scroll-triggered auto-loading,also provide a "Load more" button as a reliable alternative for keyboard users.

Implementation Checklist


Source (English):Feed Pattern — W3C APG(opens in a new tab)