How to build an accessible InfiniteScroll in React
The trigger is a real button, not a bare sentinel, so the next page is reachable by keyboard and by screen reader rather than only by scrolling — the standing accessibility complaint against scroll-only infinite lists. The IntersectionObserver activates the same code path the button does, so pointer and assistive-technology users converge on identical behaviour. While a page is in flight the button is replaced by a role="status" region carrying the loading string from the i18n catalog, which announces politely. Re-entry is guarded on a loading flag, so a sentinel still in view after a short page cannot loop. Because the component requires client JavaScript, server-render the first page as real content rather than relying on it.
When to use a InfiniteScroll
- A long, page-fetched feed or list where the user should keep reading without a page break
- Search or media results that stream in as the user scrolls
- Mobile screens where numbered pagination would be awkward to hit
When not to use it
- Content the user needs to navigate back into by position — use Pagination so URLs stay addressable
- Lists with a footer below them, which infinite loading makes unreachable
- A fixed, fully-loaded dataset — render it, or virtualize it, instead of faking pages
Keyboard interactions
Role button, verified at WCAG 2.2-AA.
EnterSpace
Common mistakes
Avoid: Leaving `disabled` false after the last page
Prefer: Pass `disabled={!hasMore}` so the observer disconnects
A sentinel that stays in view keeps re-firing `onLoadMore` against an exhausted source
Avoid: Rendering it outside the element that scrolls
Prefer: Place it as the last child of the scrolling region
The observer watches the sentinel against the nearest scroll root; outside it, intersection never changes
Example
<InfiniteScroll onLoadMore={loadNextPage} />