# usePagination

> Turns a page and a page count into the list of pages and ellipses to render, keeping its length steady as the page moves.

Docs: https://hextaui.com/docs/use-pagination
Markdown: https://hextaui.com/docs/use-pagination.md

```tsx title="components/examples/use-pagination/demo.tsx"
"use client"

import * as React from "react"
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { usePagination } from "@/hooks/use-pagination"

export function UsePaginationDemo() {
  const [page, setPage] = React.useState(6)
  const [siblings, setSiblings] = React.useState(1)
  const pagination = usePagination({ page, count: 20, siblings })

  return (
    <div className="flex w-full flex-col items-center gap-6">
      <nav aria-label="Pagination" className="flex items-center gap-1">
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label="Previous page"
          disabled={!pagination.hasPrevious}
          onClick={() => setPage(pagination.page - 1)}
        >
          <IconChevronLeft className="rtl:rotate-180" />
        </Button>
        {pagination.items.map((item) =>
          item.type === "ellipsis" ? (
            <span
              key={item.position}
              aria-hidden="true"
              className="w-8 text-center text-sm text-muted-foreground"
            >
              …
            </span>
          ) : (
            <Button
              key={item.page}
              variant={item.page === pagination.page ? "secondary" : "ghost"}
              size="icon-sm"
              aria-current={item.page === pagination.page ? "page" : undefined}
              onClick={() => setPage(item.page)}
            >
              {item.page}
            </Button>
          )
        )}
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label="Next page"
          disabled={!pagination.hasNext}
          onClick={() => setPage(pagination.page + 1)}
        >
          <IconChevronRight className="rtl:rotate-180" />
        </Button>
      </nav>
      <div className="flex items-center gap-3 text-sm text-muted-foreground">
        siblings
        <ToggleGroup
          size="sm"
          variant="outline"
          value={[String(siblings)]}
          onValueChange={(value) => {
            if (value[0]) {
              setSiblings(Number(value[0]))
            }
          }}
        >
          <ToggleGroupItem value="0">0</ToggleGroupItem>
          <ToggleGroupItem value="1">1</ToggleGroupItem>
          <ToggleGroupItem value="2">2</ToggleGroupItem>
        </ToggleGroup>
      </div>
      <code className="font-mono text-xs text-muted-foreground">
        {pagination.items.length} items
      </code>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/use-pagination.json
```

This adds the hook and anything it depends on.

### Manual

Copy and paste the following code into your project.

```ts title="hooks/use-pagination.ts"
import * as React from "react"

type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }

type UsePaginationOptions = {
  page?: number
  count: number
  siblings?: number
  boundaries?: number
}

function clampInt(value: unknown, fallback: number, min: number, max: number) {
  const number = Math.floor(Number(value))
  if (!Number.isFinite(number)) {
    return fallback
  }
  return Math.min(max, Math.max(min, number))
}

function range(start: number, end: number) {
  const pages: number[] = []
  for (let page = start; page <= end; page++) {
    pages.push(page)
  }
  return pages
}

function usePagination({
  page,
  count,
  siblings = 1,
  boundaries = 1,
}: UsePaginationOptions) {
  const total = clampInt(count, 0, 0, Number.MAX_SAFE_INTEGER)
  const current = total === 0 ? 0 : clampInt(page, 1, 1, total)
  const siblingCount = clampInt(siblings, 1, 0, 10)
  const boundaryCount = clampInt(boundaries, 1, 0, 10)

  const items = React.useMemo<PaginationItemData[]>(() => {
    if (total === 0) {
      return []
    }
    const start = range(1, Math.min(boundaryCount, total))
    const end = range(
      Math.max(total - boundaryCount + 1, boundaryCount + 1),
      total
    )
    const siblingsStart = Math.max(
      Math.min(
        current - siblingCount,
        total - boundaryCount - siblingCount * 2 - 1
      ),
      boundaryCount + 2
    )
    const siblingsEnd = Math.min(
      Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),
      end.length > 0 ? end[0] - 2 : total - 1
    )
    const pages: (number | "start" | "end")[] = [
      ...start,
      ...(siblingsStart > boundaryCount + 2
        ? (["start"] as const)
        : boundaryCount + 1 < total - boundaryCount
          ? [boundaryCount + 1]
          : []),
      ...range(siblingsStart, siblingsEnd),
      ...(siblingsEnd < total - boundaryCount - 1
        ? (["end"] as const)
        : total - boundaryCount > boundaryCount
          ? [total - boundaryCount]
          : []),
      ...end,
    ]
    const seen = new Set<number | string>()
    return pages
      .filter((item) => {
        if (seen.has(item)) {
          return false
        }
        seen.add(item)
        return typeof item === "string" || (item >= 1 && item <= total)
      })
      .map((item) =>
        typeof item === "string"
          ? { type: "ellipsis", position: item }
          : { type: "page", page: item }
      )
  }, [total, current, siblingCount, boundaryCount])

  return {
    page: current,
    count: total,
    items,
    hasPrevious: current > 1,
    hasNext: current < total,
  }
}

export { usePagination }
export type { PaginationItemData, UsePaginationOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { usePagination } from "@/hooks/use-pagination"
```

```tsx
const { items, page, hasPrevious, hasNext } = usePagination({
  page: currentPage,
  count: totalPages,
})

items.map((item) =>
  item.type === "ellipsis" ? (
    <span key={item.position}>…</span>
  ) : (
    <a key={item.page} href={`?page=${item.page}`}>{item.page}</a>
  )
)
```

The hook only does the math. It returns the list of pages and ellipses to render and leaves the markup to you, which is how `Pagination` builds its links. Use it to build your own pager, like dots for a carousel or a page picker in a table footer.

## How it works

The list always shows the first and last `boundaries` pages, and `siblings` pages on each side of the current one. An ellipsis fills any gap of two pages or more. A gap of exactly one page shows that page instead, because an ellipsis there would hide no more than it takes up.

```bash
count: 20, siblings: 1, boundaries: 1

page 1    1  2  3  4  5  …  20
page 6    1  …  5  6  7  …  20
page 20   1  …  16 17 18 19 20
```

Once there are enough pages, the list always has `2 × boundaries + 2 × siblings + 3` items. Near the ends, the window widens instead of shrinking. Because the length never changes, the pager keeps its width, and the next and previous buttons stay under the pointer as you click through.

- `count` and `page` are clamped: a page past the end becomes the last page, and anything that isn't a finite number falls back to the default.
- A `count` of 0 returns no items and a `page` of 0, so an empty table can render nothing without a special case.
- `siblings` and `boundaries` go from 0 to 10.
- Ellipses have a stable `position` of `start` or `end`. Use it as the React key.

## Examples

### Dots

With `boundaries: 0` the list is just a window around the current page. Ellipses become small dots, so a long set of slides never needs more than five targets.

```tsx title="components/examples/use-pagination/dots.tsx"
"use client"

import * as React from "react"

import { usePagination } from "@/hooks/use-pagination"

export function UsePaginationDots() {
  const [page, setPage] = React.useState(1)
  const { items } = usePagination({
    page,
    count: 12,
    siblings: 1,
    boundaries: 0,
  })

  return (
    <nav aria-label="Slides" className="flex items-center gap-1">
      {items.map((item) =>
        item.type === "ellipsis" ? (
          <span
            key={item.position}
            aria-hidden="true"
            className="size-1 rounded-full bg-border"
          />
        ) : (
          <button
            key={item.page}
            type="button"
            aria-label={`Slide ${item.page}`}
            aria-current={item.page === page ? "true" : undefined}
            onClick={() => setPage(item.page)}
            className="flex size-6 items-center justify-center rounded-full outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
          >
            <span className="h-2 w-2 rounded-full bg-muted-foreground/30 transition-all duration-300 ease-out-quint in-aria-[current=true]:w-5 in-aria-[current=true]:bg-foreground motion-reduce:transition-none" />
          </button>
        )
      )}
    </nav>
  )
}
```

## Good to know

- Mark the current page with `aria-current="page"` and wrap the list in a `<nav>` with a label.
- Hide ellipses from screen readers with `aria-hidden`. They carry no information that the page numbers don't.
- Show page numbers with `tabular-nums` so the buttons don't change width as the digits change.

## API reference

### usePagination(options)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `count` | `number` | – | Total number of pages. |
| `page` | `number` | `1` | The current page, starting at 1. |
| `siblings` | `number` | `1` | Pages to show on each side of the current page. |
| `boundaries` | `number` | `1` | Pages to always show at the start and the end. |

### Returns

| Property | Description |
| --- | --- |
| `items` | PaginationItemData[] to render, in order. |
| `page` | The clamped current page. |
| `count` | The clamped page count. |
| `hasPrevious` | Whether there's a page before this one. |
| `hasNext` | Whether there's a page after this one. |

```tsx
type PaginationItemData =
  | { type: "page"; page: number }
  | { type: "ellipsis"; position: "start" | "end" }
```

### Used by

`Pagination`.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
