# Pagination

> A React pagination for tables and lists: numbered pages with ellipses, previous / next, page size, jump to page and load more.

- Type: React component (Navigation)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add pagination`
- Docs: https://wingo-ui.com/components/pagination

## Interaction

Click a page number and the current-page pill slides to it on a quick spring; previous and next step one page and grey out at the ends, the optional « » buttons jump to the first and last page. With the keyboard, Tab reaches every control, and while focus is on the pages Left / Right go to the previous / next page and keep focus on the new current page, Home / End jump to the first / last. Type a number in "Go to" and press Enter to jump (it clamps to the real range). The page size select keeps the first row you were looking at on screen. The summary ("21 to 40 of 318") is announced once per change. When it is narrower than 640px (a phone, a card, a side panel) it becomes ‹ Page 5 of 16 › with the summary under the text: tap it for a list of every page (a bottom sheet on phones); buttons are 44px on touch. The load-more variant shows how many are left and a thin progress bar, and with autoLoad it loads on its own when you scroll within 200px of it.

## Installation

```bash
npx wingo-ui@latest add pagination
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/pagination` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/pagination.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useScrollState](https://wingo-ui.com/components/use-scroll-state) ([Markdown](https://wingo-ui.com/components/use-scroll-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

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

export function InvoicesFooter({ total }: { total: number }) {
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(20)
  return (
    <Pagination
      total={total}
      page={page}
      onPageChange={setPage}
      pageSize={pageSize}
      onPageSizeChange={setPageSize}
      showPageSize
      locale="en-US"
      labels={{
        summary: (a, b, t) => `${a}\u2013${b} of ${t} invoices`,
        pageSize: "Invoices per page",
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `page?` | `number` |  | The current page, 1-based, controlled. A page beyond pageCount renders as given with next disabled; it is never corrected silently. |
| `defaultPage?` | `number` | `1` | The starting page while uncontrolled. |
| `onPageChange?` | `(page: number) => void` |  | Fires on every page change: a click, an arrow key, the jump field, the compact page picker or a page size change. |
| `pageCount?` | `number` |  | Total pages. Default: ceil(total / pageSize). 0 renders only the summary ("No results"). |
| `total?` | `number` |  | The item count, for the page count and the summary ("1 to 20 of 318"). Without it the summary reads "Page 5 of 16". |
| `pageSize?` | `number` |  | Items per page, controlled. |
| `defaultPageSize?` | `number` | `"20"` | Items per page while uncontrolled; also used when total is given without pageSize. |
| `onPageSizeChange?` | `(pageSize: number) => void` |  | Fires when the page size select changes. The page moves so the first visible item stays on screen: page = floor(firstIndex / newSize) + 1. |
| `pageSizeOptions?` | `number[]` | `"[10, 20, 50, 100]"` | The choices in the page size select. |
| `variant?` | `"numbers" \| "simple" \| "compact" \| "load-more"` | `"numbers"` | numbers: ‹ 1 … 4 5 6 … 20 ›. simple: Previous / Next buttons with the summary between. compact: ‹ Page 5 of 20 › where the text opens a page picker. load-more: a full-width button for feeds with a remaining count and a progress bar. |
| `mobileVariant?` | `"compact" \| "simple" \| "numbers"` | `"compact"` | What renders while the pagination is narrower than 640px: a phone, or a card or side panel on desktop (a CSS container query). Both layouts are in the server HTML and CSS picks one, so nothing jumps after hydration. numbers drops the sibling pages there so it fits 360px. |
| `siblingCount?` | `number` | `1` | Pages shown on each side of the current one. The slots come from getPageRange(page, pageCount, siblingCount, boundaryCount), exported for custom UIs; their count stays constant while the page moves. |
| `boundaryCount?` | `number` | `1` | Pages always shown at each end. |
| `showFirstLast?` | `boolean` | `false` | Adds « and » buttons that jump to the first and last page. Below 640px the numbers row drops them (its boundary pages already reach the ends) so it fits 360px. |
| `showSummary?` | `boolean` | `true` | Shows "1 to 20 of 318" (a polite live region, so screen readers hear each change once). |
| `showPageSize?` | `boolean` | `false` | Shows the page size select ("20 per page"); a bottom sheet on phones. |
| `showJump?` | `boolean` | `false` | Shows a small "Go to" number field: Enter jumps, out-of-range numbers clamp. Hidden on phones when the compact picker is there. |
| `align?` | `"start" \| "center" \| "end" \| "between"` | `"between"` | between puts the summary at the start and the pages at the end; the others keep them together. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Button height 32 / 40 / 48px (md is 44px on touch). Page buttons are square and grow for long numbers. |
| `activeVariant?` | `"soft" \| "solid" \| "outline"` | `"soft"` | The current page's pill: a soft tint (like tabs), the solid tone fill, or an outline. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of every button, scaled with the size like the button's; full makes round page pills. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The current page's color (and the load-more button and bar). Neutral is black in light mode and white in dark mode. |
| `color?` | `string` | `""` | Any CSS color for the current page; replaces the tone. Leave empty to use the tone. |
| `loading?` | `boolean` | `false` | The next page is loading: the control you pressed shows a spinner and the rest are disabled until it turns false. |
| `disabled?` | `boolean` | `false` | Dims and blocks every control. |
| `autoLoad?` | `boolean` | `false` | load-more only: loads the next batch on its own when the button scrolls within 200px (IntersectionObserver in the nearest scroll container). |
| `locale?` | `string` | `""` | Number formatting ("1.250" in de-DE, "1,250" in en-US). Empty uses the LocaleProvider, then the page language. |
| `getPageHref?` | `(page: number) => string` |  | Renders every page as a link (NavLink through the app's router link) instead of a button: crawlable, open in a new tab, router-friendly. Arrow keys, the jump field and the page picker navigate too. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The link component for this instance; default the LinkProvider's, else a plain <a>. |
| `hasNextPage?` | `boolean` |  | Cursor pagination without a known count: when set and neither total nor pageCount is, only previous / next render, next disabled when false. |
| `hasMore?` | `boolean` |  | load-more: whether there is more to load. Default loadedCount < total. false shows the end message. |
| `onLoadMore?` | `(() => void \| Promise<unknown>)` |  | load-more: called by the button (or autoLoad). Return a promise to show the loading state until it settles. |
| `loadedCount?` | `number` |  | load-more: items already on screen, for "Showing 40 of 318", the remaining count and the bar. |
| `labels?` | `Partial<PaginationLabels>` |  | Every string, English by default: navigation, previous, next, first, last, page(n), pageOf(page, count), summary(from, to, total), empty, pageSize, perPage(size), jump, loadMore(count), remaining(count), loading, end, showing(shown, total). Numbers arrive formatted for the locale. DEFAULT_PAGINATION_LABELS is exported to spread and override. |
| `classNames?` | `Partial<Record<"meta" \| "summary" \| "controls" \| "list" \| "item" \| "link" \| "ellipsis" \| "indicator" \| "previous" \| "next" \| "first" \| "last" \| "size" \| "jump" \| "compact" \| "loadMore" \| "loadMoreStatus" \| "loadMoreBar", string>>` |  | Extra classes for the inner parts. |

## Parts

### getPageRange (function)

getPageRange(page, pageCount, siblingCount = 1, boundaryCount = 1): the visible slots, like [1, "start-ellipsis", 4, 5, 6, "end-ellipsis", 20], for building your own pager.

### DEFAULT_PAGINATION_LABELS (constant)

The English strings: previous, next, first, last, the summary, page size, jump, load more and the screen reader page names. Override keys through labels.

### PaginationRangeItem (type)

One slot from getPageRange: a page number, "start-ellipsis" or "end-ellipsis".

### PaginationProps (type)

The props of Pagination, for wrappers that connect it to your router or data hook.

## Examples

- **Four variants**: numbers, simple, compact and load-more, the same data.
- **Admin table footer**: Page size, jump to page and first / last buttons. Changing the size keeps your first row on screen.
- **Pages as links**: getPageHref renders real links through the router: crawlable and fine in a new tab. The address bar follows.
- **Load more in a feed**: autoLoad loads before you reach the end; the bar and the remaining count follow.
- **Cursor pagination**: No total, just hasNextPage: only previous and next, with a spinner on the one you pressed.
- **Sizes, radius and color**: 32, 40 and 48px, a brand solid pill and an outlined custom color.
- **ro-RO and en-US**: The same 24,980 results formatted for each locale.

## See also

- [Data Table](https://wingo-ui.com/components/data-table) ([Markdown](https://wingo-ui.com/components/data-table.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Toolbar](https://wingo-ui.com/components/toolbar) ([Markdown](https://wingo-ui.com/components/toolbar.md))
- [Button Group](https://wingo-ui.com/components/button-group) ([Markdown](https://wingo-ui.com/components/button-group.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))

---

Canonical: https://wingo-ui.com/components/pagination
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
