Pagination
Page numbers with previous and next: the first and last page, the pages around the current one, and an ellipsis for the rest.
- The first and last pages always show, with the current page and its neighbours (none on a phone) between them and … for the gaps; the row keeps the same number of slots on every page.
- Previous is disabled on the first page, Next on the last.
- Tab goes through the buttons; each is read as “Page 4”, and the current one as the current page.
Field marks
- Import
import { Pagination } from "cennge-ui";- Size
- 2.3 KB gzip for this import, its dependencies aside
- Pattern
- A named
<nav>of buttons;aria-current="page"on the current one - Keyboard
- Tab Enter Space
- State
- Controlled with
pageandonPageChange, ordefaultPage - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Pages
- Buttons, not links: change the address yourself in
onPageChangeif you need to - Slots
siblings * 2 + 5, seven by default- Tests
- Unit tests
When to use#
Pagination splits a long list into pages and moves between them: a commit history, search results, the rows of a table. It suits lists that people jump around in and come back to, since a page number is a place they can return to.
Similar#
- Stepper leads through the steps of one task in order, each step with its own content; Pagination moves through more of the same.
- Breadcrumbs lead up a hierarchy; Pagination moves along one level of it.
Pages of a list#
Keep the page in state and slice the list with it. Pages count from 1, and a page outside 1 to total is shown as the nearest one. With TanStack Table, pass table.getPageCount() as total, pageIndex + 1 as page, and call table.setPageIndex(page - 1) from onPageChange.
src/components: 59 folders, page 1 of 5
- accordion/
- alert/
- alert-dialog/
- avatar/
- badge/
- breadcrumbs/
- button/
- calendar/
- card/
- carousel/
- checkbox/
- combobox/
import { useState } from "react";
import { Pagination } from "cennge-ui";
const folders = [
"accordion", "alert", "alert-dialog", "avatar", "badge", "breadcrumbs", "button", "calendar", "card", "carousel",
"checkbox", "combobox", "command", "context-menu", "date-picker", "date-range-picker", "dialog", "drawer",
"dropdown", "empty-state", "file-upload", "form", "hover-card", "indicator", "input", "kbd", "listbox",
"navigation-menu", "number-input", "otp-input", "pagination", "password-input", "popover", "progress", "prose",
"radio", "rating", "scroll-area", "select", "separator", "sidebar", "skeleton", "slider", "snippet", "spinner",
"stepper", "switch", "table", "tabs", "tags-input", "textarea", "theme", "time-input", "timeline", "toast",
"toggle-group", "tooltip", "tree", "virtual-list",
];
const pageSize = 12;
export default function FolderPages() {
const [page, setPage] = useState(1);
const total = Math.ceil(folders.length / pageSize);
return (
<div className="flex w-full max-w-md flex-col gap-4">
<p className="text-sm text-muted">
src/components: {folders.length} folders, page {page} of {total}
</p>
<ul className="grid min-h-36 grid-cols-2 content-start gap-x-6 gap-y-1.5 font-code text-[13px] sm:grid-cols-3">
{folders.slice((page - 1) * pageSize, page * pageSize).map((folder) => (
<li key={folder} className="truncate">
{folder}/
</li>
))}
</ul>
<Pagination aria-label="Folder pages" total={total} page={page} onPageChange={setPage} />
</div>
);
}Narrow screens#
siblings sets how many pages show on each side of the current one, and with it the number of slots. At the default size, seven slots and two arrows take about 380 pixels, more than a phone has once the page has its margins, and the buttons wrap onto a second line. siblings={0} keeps five slots: the first and last pages, the current one and two ellipses.
import { Pagination } from "cennge-ui";
export default function NarrowPagination() {
return <Pagination aria-label="Release pages" total={40} defaultPage={20} siblings={0} />;
}Colours, sizes and disabled#
color paints the current page, primary by default, so it follows the accent. size and radius take the Button's values, and showControls={false} drops the arrows. isDisabled dims every button and ignores the pointer, for instance while the next page loads.
import { Pagination } from "cennge-ui";
export default function PaginationStyles() {
return (
<div className="flex flex-col gap-5">
<Pagination aria-label="Small, round" total={5} defaultPage={2} size="sm" radius="full" color="secondary" />
<Pagination aria-label="Without arrows" total={5} defaultPage={2} showControls={false} />
<Pagination aria-label="Disabled" total={5} defaultPage={2} isDisabled />
</div>
);
}Accessibility#
The control is a <nav> named "Pagination"; with more than one on a page, give each its own aria-label. The arrows are named "Previous page" and "Next page", and the ellipses are hidden from screen readers, so a reader hears only the pages there are buttons for.