Pagination
Move between pages of results. The windowing rule — how many neighbours to show and where the gaps fall — is exported as pageItems, so it can be reused or tested on its own. OffsetPagination and CursorPagination cover lists that cannot be browsed by page number: a range, Previous and Next.
src/components/ui/pagination.tsxAbout
From the component's own source, where it was written.
Move between pages of results.
pageItems is exported because the windowing rule — how many neighbours to show, and where gaps fall — is the part worth testing and reusing, not the markup around it.
Install
npx astralyx-ui add pagination
Usage
import { Pagination } from '@/components/ui/pagination'
const [page, setPage] = useState(1)
<Pagination page={page} count={20} onPageChange={setPage} />Composer
Change the props and the source below follows.
<Pagination
page={page}
count={20}
onPageChange={setPage}
/>Examples
Default
<Pagination page={page} count={20} onPageChange={setPage} />Few pages — no gaps
<Pagination page={page} count={5} onPageChange={setPage} />More siblings
<Pagination page={page} count={100} siblings={2} onPageChange={setPage} />Compact
<Pagination page={page} count={20} compact onPageChange={setPage} />By offset
const [offset, setOffset] = useState(0)
<OffsetPagination offset={offset} pageSize={20} total={57} onOffsetChange={setOffset} />By cursor, with a capped total
const pages = useCursorPagination(filtersKey)
const { data } = useQuery(['rows', filtersKey, pages.cursor], …)
<CursorPagination
page={pages.page}
pageSize={25}
rows={data?.rows.length}
total={data?.total}
totalCapped={data?.totalCapped}
nextCursor={data?.nextCursor}
onNext={pages.next}
onPrevious={pages.previous}
/>API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
Pagination
Extends Omit<ComponentProps<'nav'>, 'onChange'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| page | number | required | Current page, 1-based. Controlled — the component holds no state. |
| count | number | required | Total number of pages. |
| onPageChange | (page: number) => void | — | Fires with the requested page. Out-of-range requests are filtered out first. |
| siblings | number | 1 | Neighbours shown either side of the current page. First and last are always present. |
| size | ButtonProps['size'] | 'icon-sm' | Button size for the controls. |
| compact | boolean | false | Drop the numbers and show "page / count" between the arrows. |
| label | string | 'Pagination' | Accessible name for the nav landmark. |
| previousLabel | string | 'Previous page' | — |
| nextLabel | string | 'Next page' | — |
| pageLabel | (page: number) => string | DEFAULT_PAGE_LABEL | Accessible name for a page button. |
OffsetPagination
"21–40 of 57" with Previous and Next, for a list paged by offset and pageSize. Renders nothing while everything fits on one page, and when the total shrinks under the current page — rows deleted, a stale link — moves to the last page that still holds rows.
Extends Omit<ComponentProps<'nav'>, 'onChange'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| pageSize | number | required | Rows per page. |
| size | ButtonProps['size'] | 'icon-sm' | Button size for the controls. |
| label | string | — | Accessible name for the nav landmark. |
| previousLabel | ReactNode | — | — |
| nextLabel | ReactNode | — | — |
| rangeLabel | (range: RangeParts) => ReactNode | — | The range line, from numbers already formatted for locale. Defaults to "21–40 of 57". |
| unknownLabel | ReactNode | — | Shown in place of the range while the total is still loading. |
| locale | string | — | Formats the numbers in the range line. |
| offset | number | required | — |
| total | number | required | Undefined while it loads. |
| onOffsetChange | (offset: number) => void | required | — |
| maxOffset | number | — | The deepest offset the API accepts. Next stops there, and an offset past it is moved back. |
CursorPagination
The same line for a keyset-paged list, which knows only its page index, the rows on it and the next cursor. totalCapped shows a total counted up to a cap as "10,000+".
Extends Omit<ComponentProps<'nav'>, 'onChange'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| pageSize | number | required | Rows per page. |
| size | ButtonProps['size'] | 'icon-sm' | Button size for the controls. |
| label | string | — | Accessible name for the nav landmark. |
| previousLabel | ReactNode | — | — |
| nextLabel | ReactNode | — | — |
| rangeLabel | (range: RangeParts) => ReactNode | — | The range line, from numbers already formatted for locale. Defaults to "21–40 of 57". |
| unknownLabel | ReactNode | — | Shown in place of the range while the total is still loading. |
| locale | string | — | Formats the numbers in the range line. |
| page | number | required | Current page, 1-based. Controlled — the component holds no state. |
| rows | number | required | Rows on the page shown. Undefined while it loads. |
| total | number | required | — |
| totalCapped | boolean | false | The total was counted only up to a cap — shown as "10,000+". |
| nextCursor | string | required | The cursor of the page after this one. None means this is the last. |
| onNext | (cursor: string) => void | required | — |
| onPrevious | () => void | required | — |
Hooks
| Hook | Signature | Description |
|---|---|---|
| useCursorPagination | (resetKey: string) => { cursor: string; page: number; next: (cursor: string) => void; previous: () => void; first: () => void; } | The trail of cursors behind the current page — a cursor API has no way back, so it is remembered here. A new resetKey (the filters changed) starts again at the first page. |
Functions
| Function | Signature | Description |
|---|---|---|
| pageItems | (page: number, count: number, siblings = 1) => PageItem[] | The exported windowing function behind the row. |
| lastPageOffset | (total: number, pageSize: number) => number | The offset of the last page holding anything. |
Types
| Type | Definition | Description |
|---|---|---|
PageItem | number | 'gap' | Move between pages of results. |
RangePaginationProps | Omit<ComponentProps<'nav'>, 'onChange'> & {
/** Rows per page. */
pageSize: number
size?: ButtonProps['size']
/** Accessible name for the nav landmark. */
label?: string
previousLabel?: ReactNode
nextLabel?: ReactNode
/** The range line, from numbers already formatted for `locale`. */
rangeLabel?: (range: RangeParts) => ReactNode
/** Shown in place of the range while the total is still loading. */
unknownLabel?: ReactNode
/** Formats the numbers in the range line. */
locale?: string
} | — |