Layout
Infinite scroll
An `IntersectionObserver` sentinel: the "reached the edge of the list" event, as a component.
Import
import { InfiniteScroll } from '@the_viveksingh/vivek-ui'Load as the edge approaches
An IntersectionObserver sentinel fires onLoadMore 256px before the edge, guards re-entry while the promise is pending, and renders endContent when hasMore goes false - an ending you can see. Where the observer API is missing, a real "Load more" button renders instead, so a keyboard user is never stranded.
Item 1 of 40
Item 2 of 40
Item 3 of 40
Item 4 of 40
Item 5 of 40
Item 6 of 40
Item 7 of 40
Item 8 of 40
const [items, setItems] = useState(firstPage)
const [hasMore, setHasMore] = useState(true)
<InfiniteScroll
hasMore={hasMore}
onLoadMore={async () => {
const next = await api.page(items.length)
setItems((current) => [...current, ...next.items])
setHasMore(next.hasMore)
}}
endContent={<Text tone="muted">You have reached the end.</Text>}
>
{items.map((item) => <Row key={item.id} {...item} />)}
</InfiniteScroll>
// inverse puts the sentinel at the START - chat history backfills upward:
<InfiniteScroll inverse hasMore={hasOlder} onLoadMore={loadOlder}>...</InfiniteScroll>Props
Generated from the package's own type declarations, so this table cannot drift from the code.
| Prop | Type | Default | Description |
|---|---|---|---|
onLoadMore required | () => void | Promise<void> | — | Required. Called when the sentinel scrolls into view (or the fallback button is pressed). Return a promise and the component holds off until it settles — the classic infinite-scroll bug is one scroll event firing five overlapping fetches for the same page, and the guard here is what makes that impossible. |
hasMore required | boolean | — | Required. While `true` the sentinel is armed; when it turns `false` the observer disconnects and `endContent` renders instead. An explicit prop rather than an inference, because only the caller knows whether the last response was the last page. |
loader | ReactNode | — | Shown while a load is pending. Defaults to a small indeterminate indicator. |
endContent | ReactNode | — | Rendered once `hasMore` is `false`. Make it visible text ("You're all caught up") — a list that simply stops loading is indistinguishable from a list that broke. |
inverse | boolean | — | Put the sentinel at the START of the content instead of the end — chat history, where scrolling *up* fetches older messages. Pairs with `ChatThread`. |
rootMargin | string | — | How far outside the viewport the sentinel counts as "in view". Default `'256px'`, so the next page starts loading before the user reaches the edge and never actually sees the loader on a fast connection. |
loadMoreButton | boolean | — | Render a real "Load more" button that shares the pending guard. Defaults to `true` exactly when `IntersectionObserver` is missing, so a broken or absent observer degrades to a working control instead of a list that silently ends. Force it `true` to offer both paths, or `false` to suppress the fallback entirely. |
Every remaining prop is spread onto the root element, so all standard HTML and ARIA attributes work. className and style are merged with the library's own, never replaced, and the ref forwards to the root DOM node.
Rendering
InfiniteScroll declares 'use client' because it needs state, effects or event handlers. Importing it into a Server Component creates a client boundary at this component — everything above it stays on the server.