A primitive that creates all the reactive data to manage your pagination.
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 3 | UI Patterns | 1.0.0-next.6 (next) | Aug 13, 2026 | Demo → |
npm i @solid-primitives/pagination@nextA primitive that creates all the reactive data to manage your pagination:
createPagination- Provides an array with the properties to fill your pagination with and a page setter/getter.createSegment- Provides a reactive segment of an array (e.g. a page of a number of items).createInfiniteScroll- Provides an easy way to implement infinite scrolling.
createPagination
Provides an array with the properties to fill your pagination with and a page setter/getter.
How to use it
type PaginationOptions = { /** the overall number of pages */ pages: number; /** the highest number of pages to show at the same time */ maxPages?: number; /** start with another page than `1` */ initialPage?: number; /** show an element for the first page */ showFirst?: boolean | ((page: number, pages: number) => boolean); /** show an element for the previous page */ showPrev?: boolean | ((page: number, pages: number) => boolean); /** show an element for the next page */ showNext?: boolean | ((page: number, pages: number) => boolean); /** show an element for the last page */ showLast?: boolean | ((page: number, pages: number) => boolean); /** content for the first page element, e.g. an SVG icon, default is "|<" */ firstContent?: JSX.Element; /** content for the previous page element, e.g. an SVG icon, default is "<" */ prevContent?: JSX.Element; /** content for the next page element, e.g. an SVG icon, default is ">" */ nextContent?: JSX.Element; /** content for the last page element, e.g. an SVG icon, default is ">|" */ lastContent?: JSX.Element; /** accessible name for the first page element, default is "First page" */ firstAriaLabel?: string; /** accessible name for the previous page element, default is "Previous page" */ prevAriaLabel?: string; /** accessible name for the next page element, default is "Next page" */ nextAriaLabel?: string; /** accessible name for the last page element, default is "Last page" */ lastAriaLabel?: string; /** number of pages a large jump, if it should exist, should skip */ jumpPages?: number;};
// Returns a tuple of props, page and setPage.// Props is an array of props to spread on each button.// Page is the current page number.// setPage is a function to set the page number.
const [props, page, setPage] = createPagination({ pages: 3 });While the preferred structure is links or buttons (if only client-side) inside a nav element, you can use arbitrary components, e.g. using your favorite UI component library (as long as it supports the same handlers and properties as DOM nodes, which it probably should). The props objects for each page will be reused in order to grant maximum performance using the <For> flow component to iterate over the props:
const [paginationProps, page, setPage] = createPagination({ pages: 100 });
createEffect(() => { /* do something with */ page();});
return ( <nav class="pagination"> <For each={paginationProps()}>{props => <button {...props} />}</For> </nav>);In order to allow linking the pages manually, there is a non-enumerable page property in the props object:
const [paginationProps, page, setPage] = createPagination({ pages: 100 });
createEffect(() => { /* do something with */ page();});
return ( <nav class="pagination"> <ul> <For each={paginationProps()}> {props => ( <li> <A href={`?page=${props.page}`} {...props} /> </li> )} </For> </ul> </nav>);TODO
- Jump over multiple pages (e.g. +10/-10)
- options for aria-labels
- optional: touch controls
createSegment
It is a common requirement to put multiple items on a single page, which exactly is what createSegment is for.
const segment = createSegment(items, limit, page);
return <For each={segment()}>{item => <Item item={item} />}</For>;itemscan be any array of items or an accessor with an array of items; even if the array increases in size, the segment will only change if the growth brings an actual changelimitis the limit for the number of items within a segment; this can be a number or an accessor containing a numberpageis an accessor with the number or the segment page, starting with 1
createInfiniteScroll
Combines IntersectionObserver with a page-based fetcher to provide an easy way to implement infinite scrolling. The sentinel-based auto-loading is browser-only, but fetching itself isn't: pass initialPageCount to request pages during SSR too, so the first page(s) are present in the server-rendered HTML for SEO and perceived speed.
Each page is its own independent async unit, so it can be rendered with <Loading>/<Errored> for idiomatic suspense and retry, or read via plain fetching/error signals if you'd rather not use boundaries.
How to use it
// fetcher: (page: number) => Promise<T[]>const [pages, setEl, { end }] = createInfiniteScroll(fetcher);
return ( <div> <For each={pages()}> {page => ( // Note: use `page.retry`, not Errored's own `reset` — `content` only // re-fetches when `retry()` bumps its internal version signal, so a // bare `reset()` would just re-surface the same cached rejection. // Errored also won't hand control back to <Loading> while that retry // is in flight, so the fallback watches `fetching()` itself. <Errored fallback={err => ( <Show when={!page.fetching()} fallback={<h4>Retrying…</h4>}> <button onClick={page.retry}>Retry: {String(err())}</button> </Show> )} > <Loading fallback={<h4>Loading…</h4>}> <For each={page.content()}>{item => <h4>{item}</h4>}</For> </Loading> </Errored> )} </For> <Show when={!end()}> <h1 ref={setEl}>Loading...</h1> </Show> </div>);Prefer plain signals over boundaries? Skip <Loading>/<Errored> and use fetching()/error()/retry() directly:
<For each={pages()}> {page => ( <Show when={!page.error()} fallback={<button onClick={page.retry}>Retry: {String(page.error())}</button>} > <Show when={!page.fetching()} fallback={<h4>Loading…</h4>}> <For each={page.content()}>{item => <h4>{item}</h4>}</For> </Show> </Show> )}</For>Definition
type InfiniteScrollPage<T> = { content: Accessor<T[]>; fetching: Accessor<boolean>; error: Accessor<unknown>; retry: () => void;};
function createInfiniteScroll<T>( fetcher: (page: number) => Promise<T[]>, options?: { initialPageCount?: number },): [ pages: Accessor<InfiniteScrollPage<T>[]>, loader: (el: Element) => void, options: { pageCount: Accessor<number>; setPageCount: Setter<number>; end: Accessor<boolean>; reset: () => void; },];pages()is the{ content, fetching, error, retry }bundle for every page requested so far, in order — feed it directly to<For>.retry()re-runs that page's fetcher and clears its error.options.initialPageCountsets how many pages are requested up front — same fetch/retry/error mechanics as any other page. Defaults to0on the server and1in the browser; raise it on the server to render initial content into the SSR'd HTML.endistrueonce a page fetch returns zero items. A failed page does not setend— the sentinel just pauses auto-loading until that page is retried.reset()disposes every page's reactive state and starts over frominitialPageCountpages. It does not abort an in-flight fetch — if one resolves or rejects after disposal, its result is ignored by the same stale-request checkretry()relies on.
Changelog
See CHANGELOG.md