{
  "title": "Pagination",
  "description": "Cross-platform pagination with auto-compact mobile variant, compound API, page-size selector, range display, and a headless usePagination hook.",
  "url": "/docs/components/pagination",
  "since": "0.0.7",
  "tags": [
    "navigation"
  ],
  "platform": "both",
  "source": "---\ntitle: Pagination\ndescription: Cross-platform pagination with auto-compact mobile variant, compound API, page-size selector, range display, and a headless usePagination hook.\nsince: 0.0.7\ntags: [navigation]\nplatform: both\ncategory: navigation\n---\n\nimport { BundleSize } from '@/components/bundle-size';\nimport { Preview } from '@/components/preview';\nimport { PropsTable } from '@/components/props-table';\n\n<BundleSize component=\"Pagination\" />\n\n## At a glance\n\n- **Two equivalent APIs**: `<Pagination page={…} pageCount={…} onPageChange={…} />` for the 80% case and a compound `Pagination.Prev` / `Pagination.Items` / `Pagination.Item` / `Pagination.Range` / `Pagination.PageSize` / `Pagination.Next` form for full layout control.\n- **Auto-compact on phones**: `variant=\"auto\"` (the default) renders numbered pages on viewports ≥ `PAGINATION_COMPACT_BREAKPOINT` (480px) and switches to a `‹ Page X of Y ›` row below — works the same on web and on iOS / Android via `useWindowDimensions`. Force one with `variant=\"numbered\" | \"compact\"`.\n- **Sibling- and boundary-aware ellipsis** (`siblingCount`, `boundaryCount`) using the same algorithm as MUI's `usePagination`, so AI agents that have learned MUI's behavior get the right defaults.\n- **Optional first / last buttons** (`showFirstLast`).\n- **`hideOnSinglePage={true}` by default** — drop in confidently and the component disappears when there's only one page of data.\n- **Built-in range display** — pass `itemCount` and `pageSize` plus `showRange` (or render `<Pagination.Range />`) to get a localized \"Showing 21–30 of 142\" string.\n- **Built-in page-size selector** — `<Pagination.PageSize options={[10, 25, 50]} />` wraps our own `Select`. Picking a new size resets the current page to 1.\n- **Jumper input** — `<Pagination.Jumper />` lets users type a page number and jump on Enter or blur; clamps out-of-range and ignores non-numeric input.\n- **`renderItem` slot** for `<Link>` integration with any router (Next.js, Expo Router, react-router) — same signature on web and native.\n- **Headless [`usePagination()`](/docs/hooks/use-pagination) hook** — the same math the component uses, exposed for fully custom UIs.\n- **WCAG 2.2 AA**: `<nav>` landmark, `aria-current=\"page\"` on the active item, debounced `aria-live=\"polite\"` page-change announcements, every label localizable via `useTranslation` (`pagination.previous`, `pagination.next`, `pagination.first`, `pagination.last`, `pagination.gotoPage`, `pagination.range`, `pagination.pageOf`, `pagination.pageSizeLabel`, `pagination.ariaLabel`).\n- **RTL** via `dir=\"rtl\"` — chevron glyphs and DOM order both flip.\n\n## Controlled vs uncontrolled — `page` and `defaultPage`\n\n`page` (paired with `onPageChange`) puts Pagination in controlled\nmode. `defaultPage` puts it in uncontrolled mode and lets the\ncomponent track the current page itself — handy for stateless lists\nwhere the URL or a parent doesn't drive the page.\n\n```tsx\n// Uncontrolled\n<Pagination defaultPage={1} pageCount={20} />\n\n// Controlled\n<Pagination page={page} pageCount={20} onPageChange={setPage} />\n```\n\n## Preview\n\n<Preview name=\"pagination-basic\" />\n\n## First / last buttons\n\nFor very long lists, surface jump-to-first and jump-to-last with `showFirstLast`.\n\n<Preview name=\"pagination-first-last\" />\n\n## Compact variant\n\nThe compact variant is what `variant=\"auto\"` switches to on phone-width screens. You can also force it on any viewport — useful for dense toolbars.\n\n<Preview name=\"pagination-compact\" />\n\n## Wider sibling and boundary windows\n\nTune how many pages are shown around the current page (`siblingCount`) and at the start / end (`boundaryCount`).\n\n<Preview name=\"pagination-siblings\" />\n\n## Range display\n\nPass `itemCount` and `pageSize` together with `showRange` to render the canonical \"Showing X–Y of Z\" string. The text is localized via `useTranslation` and updates live as the page changes.\n\n<Preview name=\"pagination-range\" />\n\n## Compound API\n\nUse compound parts when you need full control over the layout (extra buttons, custom spacing, mixing with other UI). The same context drives both APIs, so behavior matches the shorthand 1:1.\n\n<Preview name=\"pagination-compound\" />\n\n## Page-size selector\n\n`<Pagination.PageSize options={[10, 25, 50, 100]} />` wraps the project's `Select` and reads / writes the current page size from / to the root context. Picking a new size resets the page to 1 and forwards both values through `onPageChange(page, { pageSize })`.\n\n<Preview name=\"pagination-page-size\" />\n\n## Jumper input\n\nFor long lists where the user knows the page they want, drop in `<Pagination.Jumper />` — a tiny number input that jumps on Enter or blur. Out-of-range values are clamped to `[1, pageCount]` silently; non-numeric input is ignored. Pair with `Pagination.Items` for the click-driven nav.\n\n<Preview name=\"pagination-jumper\" />\n\n## Custom rendering with `renderItem`\n\nReplace the default item rendering — useful for SSR-friendly anchor tags or your router's `<Link>`. Same signature on web and native. The example below uses an `<a>` tag and prevents default navigation; in a real app you'd return your `<Link href=\"/items?page=N\">` of choice.\n\n<Preview name=\"pagination-custom-render\" />\n\n## Just the math: `usePagination`\n\nFor fully custom UIs (drag scrubbers, infinite-scroll page indicators, custom keyboard interactions), use the headless hook. It returns the same item descriptors and actions the component uses internally — without any DOM or RN elements.\n\nSee the [`usePagination`](/docs/hooks/use-pagination) hook page for the full API.\n\n<Preview name=\"pagination-hook\" />\n\n## When to use which API\n\n| Use the shorthand `<Pagination items />` when… | Use the compound `<Pagination>` children when… | Use `usePagination()` when… |\n|---|---|---|\n| You want one-line drop-in (the 80% case) | You need to mix Range / PageSize / extra UI freely | You want a totally custom UI (no `<View>` wrappers, no theming) |\n| The default layout fits | You want `<Link>` per item via `asChild` on `Pagination.Item` | You're integrating with a virtualized list, scrubber, or other non-standard UI |\n| You don't need a page-size selector | You need a page-size selector or live range text | You want to test pagination math without any rendered output |\n\n## Auto vs forced compact\n\nThe `variant` prop accepts `'auto' | 'numbered' | 'compact'`. Defaults to `'auto'` — measures `useWindowDimensions().width` once per render and swaps to `compact` below `PAGINATION_COMPACT_BREAKPOINT` (480px). The constant is exported so consumers can override it for an entire app via theme or tokens; per-instance you can simply pass `variant=\"numbered\"` or `variant=\"compact\"` to opt out.\n\n## Accessibility\n\n- The wrapper is a real `<nav>` landmark (web) / `accessibilityRole=\"navigation\"` (native), with a localized `aria-label` defaulting to `t('pagination.ariaLabel') = 'Pagination'`.\n- The current page renders `aria-current=\"page\"` and uses the `t('pagination.currentPage') = 'Current page'` label so screen readers don't confuse it with neighboring page numbers.\n- Page changes announce through a hidden `role=\"status\"` region (`aria-live=\"polite\"`) with a `Page X of Y` message, debounced 150ms so rapid clicks don't spam the screen reader.\n- Disabled prev / next / first / last expose `aria-disabled=\"true\"` and intercept presses (no callback fires).\n- The ellipsis renders `aria-hidden` and is not focusable — assistive tech sees a clean linear sequence of pages.\n- Keyboard: each page item is a `Pressable` (renders `<button>` on web, accessible role on native) so Tab moves through them in DOM order; Enter / Space activate.\n- Touch targets are at least the spacing-8 token wide and tall (≥ 32px on the default theme).\n\n## Internationalization\n\nEvery visible string is sourced from `useTranslation`. Override individual strings via the per-instance label props (`previousLabel`, `nextLabel`, `firstLabel`, `lastLabel`, `ariaLabel`) or replace the whole dictionary via your `i18nProvider`. Keys:\n\n| Key | Default |\n|---|---|\n| `pagination.ariaLabel` | \"Pagination\" |\n| `pagination.previous` | \"Previous page\" |\n| `pagination.next` | \"Next page\" |\n| `pagination.first` | \"First page\" |\n| `pagination.last` | \"Last page\" |\n| `pagination.ellipsis` | \"More pages\" |\n| `pagination.currentPage` | \"Current page\" |\n| `pagination.gotoPage` | \"Go to page \\{\\{page\\}\\}\" |\n| `pagination.range` | \"Showing \\{\\{from\\}\\}–\\{\\{to\\}\\} of \\{\\{total\\}\\}\" |\n| `pagination.pageOf` | \"Page \\{\\{page\\}\\} of \\{\\{total\\}\\}\" |\n| `pagination.pageSizeLabel` | \"Items per page\" |\n| `pagination.jumperLabel` | \"Go to page\" |\n| `pagination.jumperPlaceholder` | \"#\" |\n\n## Cursor-based pagination\n\nFor prev/next-only cursor APIs, drive the shorthand with `pageCount={hasNext ? page + 1 : page}` so the next button stays enabled while you have more cursors. Or build a tiny shell with the headless hook plus your own request state — the lib doesn't ship a separate cursor variant because the prev/next surface is identical.\n\n## URL-state sync\n\nPagination is a pure UI primitive — it does not read or write the URL on its own. To wire it to query params, hold `page` in your route's source of truth (e.g. Next's `useSearchParams` + `router.replace`, or Expo Router's `router.setParams`) and pass it back as the controlled `page` prop. See the source of any of the demos above for the controlled-state pattern.\n\n## Props\n\n<PropsTable component=\"Pagination\" />\n"
}
