Mosmox UI
Current version
GitHubnpmjs.com
Content & Media

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>
  • loadingContent replaces the built-in spinner and “Loading more” text;
  • endMessage is rendered when hasMore is false;
  • error renders the error content and pauses automatic loading;
  • onRetry adds the retry button and lets the parent clear/reload the error;
  • retryLabel sets 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

PropTypeDefaultDescription
childrenReactNoderequiredAny React content that grows as more data is loaded.
onLoadMore() => void | Promise<void>requiredCalled when the sentinel intersects the observer area.
hasMorebooleantrueEnables loading while another page is available.
loadingbooleanfalseParent-controlled loading state.
thresholdnumber0.25Intersection ratio, clamped to 0..1.
rootMarginstring'160px'CSS margin passed to IntersectionObserver.
initialLoadbooleanfalseRequests one page after mount.
disabledbooleanfalsePauses initial loading and observation.
direction'vertical' | 'horizontal''vertical'Scroll and content-flow direction.
reversebooleanfalseReverses the content flex direction.
heightnumber | string—Root max-height; numbers become pixel values.
loadingContentReactNodeBuilt-in loaderContent rendered while loading.
endMessageReactNode'All items have been loaded.'Content rendered when hasMore is false.
errorReactNode—Error content; pauses automatic loading.
onRetry() => void | Promise<void>—Enables and handles the retry button.
retryLabelstring'Try again'Retry button text and accessible label.
ariaLabelstring'Infinite scroll'Accessible label for the root section.
classNamestring—Additional root CSS class.
styleCSSProperties—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.