Overlays & navigation
Pagination
Pagination moves through pages of results. Pagination is the pager itself: previous and next, page numbers with ellipses that keep a constant width, and a compact form for phones. PaginationBar is the full table footer, adding “Showing 1–10 of 128” and a rows-per-page select. Use callbacks for client-side paging or getHref for real, shareable links.
Showing 1–10 of 128 invoices
export function InvoicesFooter() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); return ( <div className="mx-auto w-full max-w-3xl rounded-ui-lg border border-line bg-surface px-4 py-3"> <PaginationBar page={page} pageSize={pageSize} total={128} itemLabel="invoices" onPageChange={setPage} onPageSizeChange={(size) => { setPageSize(size); setPage(1); }} /> </div> );}Usage
import { Pagination, PaginationBar } from "@/components/ui/pagination"; <Pagination page={page} pageCount={24} onPageChange={setPage} />Examples
Results footer
Changing the page size resets to page 1. The summary is a polite live region, so the new range is announced.
Showing 1–10 of 128 invoices
export function InvoicesFooter() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); return ( <div className="mx-auto w-full max-w-3xl rounded-ui-lg border border-line bg-surface px-4 py-3"> <PaginationBar page={page} pageSize={pageSize} total={128} itemLabel="invoices" onPageChange={setPage} onPageSizeChange={(size) => { setPageSize(size); setPage(1); }} /> </div> );}Page numbers
The first and last pages are always shown. siblings sets how many pages appear on each side of the current one; the list keeps the same length while paging, so buttons don't jump under the pointer.
<Pagination page={page} pageCount={24} onPageChange={setPage} /><Pagination page={page} pageCount={24} onPageChange={setPage} siblings={0} />Compact
Previous, next and “Page 3 of 12”: for phones, narrow cards and infinite lists.
<Pagination page={page} pageCount={12} onPageChange={setPage} compact />As links
getHref renders Next.js links instead of buttons (in your app, return something like /invoices?page=3), so every page has a URL, works in a static export and can be opened in a new tab.
<Pagination page={3} pageCount={9} getHref={(p) => `#page-${p}`} />Props
Pagination
pagerequirednumberdefault: —Current page, starting at 1.pageCountrequirednumberdefault: —Total number of pages.onPageChange(page: number) => voiddefault: —Called with the new page (button mode).getHref(page: number) => stringdefault: —Render links instead of buttons.siblingsnumberdefault: 1Pages shown on each side of the current one.compactbooleandefault: falseOnly previous / next and “Page x of y”.
PaginationBar
page / pageSize / totalrequirednumberdefault: —Current page, rows per page and the total number of rows.onPageChangerequired(page: number) => voiddefault: —Called with the new page.onPageSizeChange(size: number) => voiddefault: —Shows the rows-per-page select when set.pageSizeOptionsnumber[]default: [10, 25, 50, 100]Choices in the select.itemLabelstringdefault: "results"Noun after the count, e.g. “invoices”.
pageRange()
pageRange(page, count, siblings?) => (number | "…")[]default: —The page list on its own, for building a custom pager.
Accessibility
- Wrapped in <nav aria-label="Pagination">. The current page has aria-current="page".
- Every control has a clear name: “Previous page”, “Next page”, “Page 7”. On phones the Previous / Next text is hidden but the names stay.
- Previous and Next are disabled (not removed) at the ends, so the layout stays put.
- Ellipses are hidden from screen readers. The “Showing …” summary updates politely after each change.