# Pagination

Numbered pages, with the middle elided.

- Group: Navigation
- Import: `import { Pagination } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/pagination/
- Related: breadcrumb

## Anatomy

- **Nav** (required) — A <nav> named by label, “Pagination” by default. Nothing else here is a landmark, so this is how a reader jumps to the pager rather than scrolling to it.
- **Step buttons** (required) — Previous and Next, pill-cornered at --control-h-sm, named by previousLabel and nextLabel — “Previous page” and “Next page” until a call site says otherwise. Each is disabled at its end of the range, which takes it out of the tab order rather than leaving a control that does nothing.
- **Page list** (required) — An <ol> of numbers, each a <button> named by pageLabel — “Page N” by default — and carrying aria-current on the one you are on.
- **Travelling pill** — One aria-hidden fill, measured from the selected button and moved with a transform rather than two grounds cross-fading. It is absent until the first measurement lands, and holds still under prefers-reduced-motion.
- **Ellipsis** — An aria-hidden <li> wherever the sequence skips more than one page. A single skipped page is printed instead — “1 … 3” is longer than “1 2 3” and says less.

## Best practices

### Do

- Move page in the same state update that fetches: it is fully controlled, so a handler that loads the next page without setting page leaves the pager marking the page the reader just left.
- Raise siblings rather than lowering it — below 2 × siblings + 5 pages every page is printed anyway, so the prop does nothing on a short list and is the only lever you have on a long one.
- Import paginationRange when something else has to agree with the pager: it is exported and pure, which is how a server-rendered summary and this component end up describing one window instead of two.
- Let the surrounding row collapse: the component returns null at one page or fewer, so a footer built to a fixed height shows an empty strip on the day the list gets short.

### Don’t

- Do not put it in a compact region and call it a touch target: the pills are --control-h-sm, 36px comfortable and 30px under data-density="compact", set 4px apart — well under the 44px WCAG 2.5.5 asks of a pointer target.
- Do not expect pageLabel to change what is printed. It names the control for a screen reader and nothing else; the button still shows the Western digit it was handed, so a locale that writes its numerals differently has to format them at the call site as well.

## Accessibility

- The current page is a button with aria-current, not a styled span — a reader jumping by control needs to find it.
- Renders nothing at one page. A pager for a single page is furniture.
- Every string a reader hears is a prop: the two chevrons by name, each numbered page through pageLabel — a function rather than a template, because “Page 3” is a phrase whose parts move around between languages.

## Keyboard

- Tab — Reaches every control, including the current page.
- Enter / Space — Goes to that page.

## paginationRange

Builds the visible page list: always the first and last page, a window around the current one, and an ellipsis wherever the sequence skips. Returned as numbers and a literal ellipsis rather than as pre-rendered nodes, so the shape is testable without a DOM — the off-by-one at the window edges is the whole difficulty of this component.

## Pagination

Numbered pagination. The current page is marked by one filled pill that TRAVELS between the numbers rather than by a background switching off on one and on on another. Two backgrounds cross-fading reads as two things changing; a shape moving reads as the one thing that actually did. A `<nav>` wrapping a list, and the current page is a `<button aria-current>` rather than a styled `<span>` — a reader jumping by landmark needs to find the control, and a reader on the current page needs to be told they are already there. Renders nothing at one page or fewer. A pager for a single page is furniture. Every string a reader hears is a prop. The chevrons carry no text, so `previousLabel` and `nextLabel` are the whole of those two controls' names, and `pageLabel` is a function because "Page 3" is a phrase whose parts move around between languages.

### Props

- `page` (required) — `number`. 1-based.
- `pageCount` (required) — `number`. Total pages. A value of 1 or less renders nothing.
- `onPageChange` (required) — `(page: number) => void`.
- `siblings` — `number` default `1`. How many numbered pages sit either side of the current one.
- `label` — `string` default `'Pagination'`.
- `previousLabel` — `string` default `'Previous page'`. Names the two step controls. Each is a chevron and nothing else, so these strings are its entire accessible name — the same reason `AppShell` exposes `openLabel` and `closeLabel` rather than baking them in.
- `nextLabel` — `string` default `'Next page'`.
- `pageLabel` — `(page: number) => string` default `(page) => `Page ${page}``. Names one numbered page. A function rather than a template, because the number does not sit in the same place in every language and neither does the noun around it — `"Page 3"`, `"第 3 页"`, `"Seite 3"`. The visible digit is the same either way; this is what a screen reader hears instead of a bare number.

Also accepts: `Omit<HTMLAttributes<HTMLElement>, 'onChange'>`.

## Example — default

```tsx
import { Pagination } from '@misoto22/folio'

<div className="flex flex-col items-center gap-5">
  <Pagination page={page} pageCount={20} onPageChange={setPage} />
  <p className="m-0 mono-meta text-(--ink-3-aa)">page {page} of 20</p>
</div>
```

## Example — the edges

```tsx
import { Pagination, Text } from '@misoto22/folio'

<div className="flex flex-col gap-6">
  <div className="flex flex-col gap-2">
    <Text size="xs" tone="muted">
      At the first page
    </Text>
    <Pagination label="First page" page={first} pageCount={24} onPageChange={setFirst} />
  </div>
  <div className="flex flex-col gap-2">
    <Text size="xs" tone="muted">
      In the middle, elided on both sides
    </Text>
    <Pagination label="Middle pages" page={middle} pageCount={24} onPageChange={setMiddle} />
  </div>
  <div className="flex flex-col gap-2">
    <Text size="xs" tone="muted">
      At the last page
    </Text>
    <Pagination label="Last page" page={last} pageCount={24} onPageChange={setLast} />
  </div>
</div>
```

## Example — a wider window

```tsx
import { Pagination, Text } from '@misoto22/folio'

<div className="flex flex-col gap-6">
  <div className="flex flex-col gap-2">
    <Text size="xs" tone="muted">
      One sibling, the default
    </Text>
    <Pagination label="Default window" page={page} pageCount={24} onPageChange={setPage} />
  </div>
  <div className="flex flex-col gap-2">
    <Text size="xs" tone="muted">
      Two siblings
    </Text>
    <Pagination
      label="Wider window"
      siblings={2}
      page={page}
      pageCount={24}
      onPageChange={setPage}
    />
  </div>
</div>
```
