Skip to content
cennge-ui
GitHubv0.3.1, changelog

Pagination

Page numbers with previous and next: the first and last page, the pages around the current one, and an ellipsis for the rest.

Commits on mainPage 3 of 12
  1. 29dd316test(theme): run on React 17 and Testing Library 12 too
  2. f614e25fix(dialog): draw the backdrop beside the dialog, not around it
  3. 9b1a5a4feat(charts): Sparkline
  4. cdb8e4efeat(charts): DonutChart
  5. 5696189feat(charts): LineChart and AreaChart
  1. 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.
  2. Previous is disabled on the first page, Next on the last.
  3. Tab goes through the buttons; each is read as “Page 4”, and the current one as the current page.
Live: click it, use the keys.

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 page and onPageChange, or defaultPage
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Pages
Buttons, not links: change the address yourself in onPageChange if 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/
list.tsx
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.

narrow.tsx
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.

styles.tsx
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.

Props#

PropTypeDefault
totalrequired
numberNumber of pages.
–
page
number
–
defaultPage
number
–
onPageChange
(page: number) => void
–
siblings
numberPages shown on each side of the current one.
1
showControls
booleanPrevious / next buttons.
true
color
"default""primary""secondary""success""warning""danger""info"Colour of the current page.
"primary"
size
"sm""md""lg"
"md"
radius
"none""sm""md""lg""full"
"md"
isDisabled
boolean
–
aria-label
stringDefines a string value that labels the current element.
"Pagination"