What is Infinite scroll?
Infinite scroll: Infinite scroll loads the next batch of content automatically as the user nears the end of a list, instead of asking them to click to another page.
Infinite scroll replaces page links with continuous loading. When the user scrolls close to the bottom of a list, the app fetches the next batch and appends it, so the list appears to have no end. Social feeds, image galleries and activity streams use it because it keeps people browsing without a decision point. It is a poor fit when people are looking for something specific, need to return to a position, or want to reach the footer.
When to use it
- Use it for browsing feeds with roughly equal items and no natural end, such as activity logs, inspiration galleries and chat history.
- Prefer [pagination](/glossary/pagination) for search results, tables, anything people compare or share a position in, and pages with important footer links.
- Consider a Load more button as a middle ground. It keeps the continuous list but lets the user decide, and the footer stays reachable.
How to build one in React
Place an invisible sentinel element after the last item and watch it with IntersectionObserver. A generous rootMargin starts the fetch before the user actually hits the bottom:
"use client"
import * as React from "react"
export function useLoadMore(onLoadMore: () => void, enabled: boolean) {
const ref = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
const el = ref.current
if (!el || !enabled) return
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) onLoadMore()
},
{ rootMargin: "600px 0px" }
)
io.observe(el)
return () => io.disconnect()
}, [onLoadMore, enabled])
return ref
}
// <ul>{items.map(...)}</ul>
// <div ref={sentinel} aria-hidden />
// {hasMore ? <button onClick={loadMore}>Load more</button> : null}
Pass enabled={hasMore && !isLoading} so the observer does not fire twice for the same page, and use cursor-based requests so new items at the top do not shift what the next page returns. Show a skeleton row or small spinner at the end while loading, and a clear end-of-list message once hasMore is false, so users know they have seen everything.
Accessibility
- Keyboard users tab through every item before reaching anything below the list, and new items keep arriving. Keep a visible Load more button as an alternative and avoid footers below infinite lists.
- The WAI-ARIA feed pattern (
role="feed"witharticlechildren,aria-setsize,aria-posinsetandaria-busywhile loading) lets screen reader users move between articles with Page Down and Page Up. - Announce loading and the end of the list in a polite live region.
- Restore scroll position when the user navigates back, or they lose their place.
Performance
Thousands of appended DOM nodes slow down scrolling and memory. Past a few hundred rows, render only what is on screen with a virtualized list, and show skeleton rows while the next page loads. Some feeds also drop items far above the viewport and fetch them again on the way back up, which keeps memory flat in long sessions. More list and table building blocks are in the data tables category.
Virtualized ListuiVirtualized list that renders only the visible rows plus a small buffer, using a fixed item height and absolute positioning. Keeps thousands of React rows fast.npx shadcn@latest add ui.minidev.pro/r/virtualized-list.jsonSkeletonuiTailwind skeleton loader with a sunken plate and a soft sheen that sweeps every 1.6s, plus a SkeletonCard preset with title, text lines and a media block.npx shadcn@latest add ui.minidev.pro/r/skeleton.json