Pagination

A React pagination control - prev/next arrows and a live range readout - for tables and search results.

Pagination preview

Install Pagination

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { Pagination } from '@zyncat/ui/pagination';

Usage

tsx
import { Pagination } from '@zyncat/ui/pagination';
 
<Pagination
range={[offset + 1, Math.min(offset + pageSize, total)]}
total={total}
hasPrev={offset > 0}
hasNext={offset + pageSize < total}
onPrev={() => setOffset(offset - pageSize)}
onNext={() => setOffset(offset + pageSize)}
/>

Compose Pagination in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

Pagination props

Pagination

PropTypeDefaultDescription
ariaLabelstring'Pagination'Accessible name for the nav landmark - name the list ("Posts"), not "pagination".
range*[number, number]—Items currently shown, 1-based inclusive: `[from, to]` (e.g. `[26, 50]`).
totalnumber | null—Total item count - render `of N` only when the API reports one; omit for endless lists.
hasPrevbooleanfalseA previous cursor exists.
hasNextbooleanfalseA next cursor exists.
onPrev() => void—Fired when the previous arrow is pressed.
onNext() => void—Fired when the next arrow is pressed.
loadingbooleanfalseA page fetch is in flight: both arrows go inert, the clicked arrow carries the spinner.
classNamestring—Extra class(es) merged onto the root element.
...htmlAttributesHTMLAttributes—Standard <nav> attributes (style, data-*, aria-*, ...) forwarded to the root.

Frequently asked questions

Splitting a long result set into pages so only one slice renders at a time, instead of the whole list at once. This component renders that control as a compact strip - a live "from-to of total" range readout next to a previous/next arrow pair - rather than a row of clickable page numbers.

Neither. There is no 1-2-3 row and no ellipsis-truncation logic - range is a [from, to] tuple and hasPrev/hasNext are plain booleans, so the component never needs to know how many pages exist. It is also not scroll-triggered: paging only happens when someone presses the previous or next arrow, which is what onPrev/onNext fire for.

Pass Pagination its own state, separate from the table: range is the [from, to] slice currently shown, total is the full row count (or omit it for an endless list), and hasPrev/hasNext gate the arrows. It is fully controlled - there is no internal page index - so onPrev/onNext just update whatever state feeds your rows, whether that is an offset, a page number or an opaque cursor.

Yes - total is optional (number | null), and hasPrev/hasNext are booleans rather than a page count, so the props map directly onto a cursor response: set them from whatever has_more or has_previous flags the API returns, and skip total entirely when the API does not provide one. Nothing in the component assumes a fixed page size or a known page count.

Set loading - both arrows go inert and the one that was just clicked (tracked internally) shows the Button spinner while the other simply disables, so the control never reflows or lets a second click queue up mid-fetch.

Yes to both. The root is a nav element with an aria-label landmark (name the list, e.g. "Posts", not the word "pagination"), the range readout is aria-live="polite" so screen readers announce new totals, and each arrow carries its own aria-label plus native button semantics. It ships with "use client" intact for the Next.js App Router, has zero runtime dependencies, and the slide animation on the range readout collapses under prefers-reduced-motion.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub