Infinite Scroll
Load additional content automatically when a sentinel approaches the end of a scroll region.
InfiniteScroll observes an invisible sentinel after its children and calls onLoadMore when that sentinel intersects the configured viewport. It does not own an array, fetch data, or append items itself: children may contain a list, cards, articles, a gallery, a grid, or any other React content, and the parent application updates that content after loading another page.
Basic usage
const [articles, setArticles] = useState(initialArticles);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const hasMore = page < totalPages;
async function loadNextPage() {
setLoading(true);
const next = await fetchArticles(page + 1);
setArticles((current) => [...current, ...next.items]);
setPage((current) => current + 1);
setLoading(false);
}
<InfiniteScroll
ariaLabel="Article feed"
hasMore={hasMore}
loading={loading}
onLoadMore={loadNextPage}
>
{articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</InfiniteScroll>children is ReactNode, not a required data-list type. This is also valid:
<InfiniteScroll onLoadMore={loadNextSection}>
<ArticleIntro />
<ArticleBody />
<RelatedStories />
</InfiniteScroll>The application owns the data, pagination state, request errors, and the decision to append or replace content.
Scroll region and direction
The default direction is vertical. Pass height to set the component's max-height; the region becomes scrollable when its content exceeds that limit. A number is converted to pixels, while a string can contain any CSS length:
<InfiniteScroll height={520} onLoadMore={loadMoreActivity}>
<ActivityFeed items={items} />
</InfiniteScroll>
<InfiniteScroll height="min(70vh, 640px)" onLoadMore={loadMoreComments}>
<CommentThread comments={comments} />
</InfiniteScroll>Without height, the component can observe the page viewport. Use horizontal mode for rails and shelves:
<InfiniteScroll
direction="horizontal"
height={220}
hasMore={hasMoreProducts}
onLoadMore={loadMoreProducts}
>
<ProductRail products={products} />
</InfiniteScroll>In horizontal mode the content is laid out in a row, overflow is horizontal, and the status region reserves horizontal space.
Trigger timing
rootMargin expands the observer area so loading can begin before the sentinel is visible. threshold is the required intersection ratio and is clamped to 0..1:
<InfiniteScroll
rootMargin="320px 0px"
threshold={0.1}
onLoadMore={loadMore}
>
<ResultGrid results={results} />
</InfiniteScroll>The defaults are rootMargin="160px" and threshold={0.25}. A larger margin starts requests earlier; a larger threshold waits for more of the sentinel to intersect.
Initial loading and disabled state
Set initialLoad to call onLoadMore once after mount. The initial request is guarded so it is not repeated by subsequent renders. disabled pauses both the initial request and IntersectionObserver loading:
<InfiniteScroll
initialLoad
disabled={!isOnline}
hasMore={hasMore}
onLoadMore={loadFirstPage}
>
<Content items={items} />
</InfiniteScroll>hasMore={false} also stops observation and displays endMessage. loading is a controlled loading signal; the component also tracks an internal loading state while an async onLoadMore call is pending, preventing duplicate requests.
Loading, end, and error content
<InfiniteScroll
loading={isLoading}
loadingContent={<LoadingRows />}
endMessage={<strong>No more results.</strong>}
error={loadError ? 'Could not load the next page.' : undefined}
onRetry={retry}
retryLabel="Load again"
onLoadMore={loadMore}
>
<Rows items={items} />
</InfiniteScroll>loadingContentreplaces the built-in spinner and “Loading more” text;endMessageis rendered whenhasMoreisfalse;errorrenders the error content and pauses automatic loading;onRetryadds the retry button and lets the parent clear/reload the error;retryLabelsets the retry button text and accessible name.
If error is present without onRetry, the error is displayed but no retry button is rendered. onLoadMore may return a Promise; request rejection is swallowed by the observer callback, so applications should expose failures through their error prop.
Reverse content
reverse applies a reversed flex direction to the content: column order is reversed vertically and row order is reversed horizontally. It is useful for newest-first message layouts or reverse-ordered rails:
<InfiniteScroll
reverse
height={480}
onLoadMore={loadOlderMessages}
>
<MessageList messages={messages} />
</InfiniteScroll>The component does not preserve scroll position or prepend data automatically; the parent remains responsible for data ordering and scroll restoration.
Accessibility and performance
The root is a <section> with aria-label and exposes aria-busy while loading. The status area uses role="status", so loading, error, retry, and end messages can be announced. The sentinel is aria-hidden because it is an implementation detail.
Use stable keys for appended content, keep pages reasonably sized, and consider virtualization for very long feeds. Infinite Scroll only schedules the next load; it does not replace caching, pagination state, virtualization, or request cancellation.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Any React content that grows as more data is loaded. |
onLoadMore | () => void | Promise<void> | required | Called when the sentinel intersects the observer area. |
hasMore | boolean | true | Enables loading while another page is available. |
loading | boolean | false | Parent-controlled loading state. |
threshold | number | 0.25 | Intersection ratio, clamped to 0..1. |
rootMargin | string | '160px' | CSS margin passed to IntersectionObserver. |
initialLoad | boolean | false | Requests one page after mount. |
disabled | boolean | false | Pauses initial loading and observation. |
direction | 'vertical' | 'horizontal' | 'vertical' | Scroll and content-flow direction. |
reverse | boolean | false | Reverses the content flex direction. |
height | number | string | — | Root max-height; numbers become pixel values. |
loadingContent | ReactNode | Built-in loader | Content rendered while loading. |
endMessage | ReactNode | 'All items have been loaded.' | Content rendered when hasMore is false. |
error | ReactNode | — | Error content; pauses automatic loading. |
onRetry | () => void | Promise<void> | — | Enables and handles the retry button. |
retryLabel | string | 'Try again' | Retry button text and accessible label. |
ariaLabel | string | 'Infinite scroll' | Accessible label for the root section. |
className | string | — | Additional root CSS class. |
style | CSSProperties | — | Additional root inline styles. |
Infinite Scroll or pagination?
Use Infinite Scroll for continuous feeds, galleries, activity histories, comments, and discovery-oriented content. Prefer explicit pagination or a “Load more” button for tables, search results, long forms, and interfaces where users need predictable navigation, bookmarkable pages, or easy access to a footer.