Infinite Scroll
A list that loads more as you scroll, down or up, with a reverse mode held in place by CSS alone
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 6
- Item 7
- Item 8
- Item 9
- Item 10
Installation
pnpm dlx shadcn@latest add @alexandrejoly/infinite-scrollUsage
import { InfiniteScroll, InfiniteScrollContent, InfiniteScrollSentinel, InfiniteScrollViewport,} from "@/components/ui/infinite-scroll";<InfiniteScroll hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} onFetchNextPage={() => fetchNextPage()}> <InfiniteScrollViewport aria-label="Items" className="h-96" role="region" tabIndex={0} > <InfiniteScrollContent> {items.map((item) => ( <div key={item.id}>{item.title}</div> ))} <InfiniteScrollSentinel /> </InfiniteScrollContent> </InfiniteScrollViewport></InfiniteScroll>Examples
Reverse
Older pages load at the top. The list opens at the bottom from the first paint, and what you are reading stays put while a page arrives.
- Message 31
- Message 32
- Message 33
- Message 34
- Message 35
- Message 36
- Message 37
- Message 38
- Message 39
- Message 40
Accessibility
The content is aria-busy while a page is being fetched, so a screen reader waits for the page instead of reading each new item.
Focus stays where it is when a page arrives. In reverse mode, the DOM stays in reading order, oldest first: only the viewport is reversed, so the reading and Tab order match what is on screen.
Give a viewport with no focusable item inside tabIndex={0}, a role such as region and an aria-label, as both examples do. Chrome and Firefox let Tab reach a scroll container on their own, but Safari does not, so without them a keyboard can’t scroll it there. Avoid role="log" on a reversed list: it would announce every older page as it loads.
Meets WCAG 2.2: 1.3.2 Meaningful Sequence (opens in a new tab)
API reference
InfiniteScroll
Shares the pagination state with its parts; it renders no element.
| Prop | Type | Default |
|---|---|---|
hasNextPageWhether another page can be fetched. | boolean | None |
isFetchingNextPageWhether a page is being fetched. The content is | boolean | None |
onFetchNextPageCalled when the sentinel scrolls into view while a next page exists and none is being fetched. | () => void | None |
reverseLoads older pages at the top, as in a chat: the viewport opens at the bottom and keeps its place while pages are added above. Default: | boolean | false |
InfiniteScrollViewport
The scroll container. Give it a height. To let the page scroll instead, leave out InfiniteScrollViewport, except in reverse mode, which needs it.
Takes every prop of div.
InfiniteScrollContent
Holds the items and the sentinel, in reading order. It is aria-busy while a page is being fetched.
Takes every prop of div.
InfiniteScrollSentinel
Place it after the last item, or before the first one in reverse mode. It holds the spinner while fetching.
Takes every prop of div.
| Prop | Type |
|---|---|
renderReplaces the rendered element, for example an | ReactElement | (props, state) => ReactElement |