affectUI › Components › Navigation

Pagination

Pages of a long list – the range you see, previous and next, page numbers with … between them and rows per page, tabular numbers that keep the row still from 1 page to 10,000.

Free and open source (MIT). Install: npx shadcn@latest add https://www.affectui.com/r/pagination.json · Import from @/components/ui/pagination

When to use

  • Under a table or list that loads one page at a time: runs, logs, eval cases, invoices
  • When people need to know how much there is – "1–25 of 1,284" – and jump to the first, last or a nearby page
  • With pageSizes, when some want to see more rows at once

When not to use

  • A feed people read top to bottom – load more as they scroll instead
  • Where a page sits in a hierarchy – use the Breadcrumb
  • A few views of the same thing – use the Segmented Control
  • A handful of rows – show them all

Usage

import { useState } from "react";
import { Pagination } from "@/components/ui/pagination";

export function RunsFooter({ total }: { total: number }) {
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(25);

  return (
    <Pagination
      page={page}
      onPageChange={setPage}
      total={total}
      pageSize={pageSize}
      pageSizes={[25, 50, 100]}
      onPageSizeChange={setPageSize}
    />
  );
}

Props

NameTypeDefaultDescription
pagenumberRequired. The page shown, from 1 – kept between 1 and the last page
onPageChange(page: number) => voidRequired. Called with the new page from the arrows, a number or a new size
totalnumberAll rows. Shows the range ("1–25 of 1,284", "0 of 0" when empty) and works out the pages
pageSizenumber20Rows on a page
pageCountnumberThe number of pages when you don't know the rows – the range becomes "Page 3 of 12"
pageSizesnumber[]Shows Rows per page with these sizes (the current one is added when it's missing)
onPageSizeChange(pageSize: number) => voidA new size was picked. onPageChange follows with the page that holds the first row you saw
siblingsnumber1Page numbers on each side of the current one; with the first, the last and two … that makes 2 × siblings + 5 slots
compactbooleanfalseOnly the range and the arrows – no page numbers
disabledbooleanfalseWhile rows load: everything stays where it is, nothing can be pressed
aria-labelstring"Pagination"Names the navigation landmark – say which list, when a page has two
...nav propsComponentProps<"nav">className, style, ref and the rest go to the <nav>

Keyboard

  • Tab: Moves through Rows per page, the arrows and the numbers
  • Enter / Space: Goes to that page – the focus follows the page to where its number is now

Accessibility

  • A <nav> landmark around a list of real buttons: Previous page, Page 1 … Page 52, Next page
  • The page you are on is marked aria-current="page"
  • At the first or last page the arrow is aria-disabled, not disabled – it keeps the focus, so Tab doesn't jump to the top
  • The range is a polite live region, so a screen reader hears "26–50 of 1,284" after each move; the … are hidden
  • Rows per page is a native <select> with its label – the phone's own picker opens on touch
  • The ring shows only under keyboard focus (:focus-visible), never after a click

Responsive

  • Tabular numbers and a fixed count of slots, each as wide as the last page's number: the row keeps its width from page 1 to 10,000
  • When the row is too narrow for the numbers, only the range and the arrows stay; the range and the pager wrap onto two lines before that
  • Touch screens get 40px buttons and 15px text
  • Right to left: the arrows turn round
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Data Table – Spreadsheet-style table: select cells and ranges, edit in place, copy and paste, undo and sort
  • Breadcrumb – Where a page sits and the way back up – links with quiet separators, the current page last, a long middle folded into a … menu, long names shortened with the full one on hover
  • Select – Pick one option in the Text Field's look – icons and a second line per option, a gliding highlight, type-ahead and a list that never gets cut off
  • Segmented Control – Switch between two to six options – a raised pill or underline that glides, stretches a little and can be dragged across, with icons, counts and badges