# Pagination

Navigation links for moving between pages of content, such as table results.

Pagination is an open-source component from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/components/pagination

**React**

**Example**

```tsx
import * as React from "react"
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  PaginationSummary,
} from "@/components/ui/pagination"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function pageRange(page: number, count: number) {
  if (count <= 7) return Array.from({ length: count }, (_, index) => index + 1)
  const start = Math.max(1, Math.min(page - 2, count - 6))
  const pages: (number | "ellipsis")[] = Array.from(
    { length: 5 },
    (_, index) => start + index
  )
  if (pages.at(-1) !== count) pages.push("ellipsis", count)
  return pages
}

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function NumberedExample() {
  const [page, setPage] = React.useState(2)
  const perPage = 5
  const count = Math.ceil(orders.length / perPage)

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <PaginationSummary>
          {(page - 1) * perPage + 1}–{Math.min(page * perPage, orders.length)} /{" "}
          {orders.length}
        </PaginationSummary>
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious
              text=""
              {...go(setPage, page - 1, page === 1)}
            />
          </PaginationItem>
          {pageRange(page, count).map((entry, index) =>
            entry === "ellipsis" ? (
              <PaginationItem key={`ellipsis-${index}`}>
                <PaginationEllipsis />
              </PaginationItem>
            ) : (
              <PaginationItem key={entry}>
                <PaginationLink
                  isActive={entry === page}
                  {...go(setPage, entry, false)}
                >
                  {entry}
                </PaginationLink>
              </PaginationItem>
            )
          )}
          <PaginationItem>
            <PaginationNext
              text=""
              {...go(setPage, page + 1, page === count)}
            />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </Orders>
  )
}

export default function PaginationDemo() {
  return <NumberedExample />
}
```

Pagination is a set of link primitives you compose around your own page state. `Pagination` is the `nav` landmark. Inside it, `PaginationContent` is a list of `PaginationItem` entries, each holding a `PaginationLink`, `PaginationPrevious`, `PaginationNext`, `PaginationFirst`, `PaginationLast` or `PaginationEllipsis`.

For compact toolbars, `PaginationGroup` joins arrow links into one segmented control, `PaginationInput` lets the user type a page number, and `PaginationSummary` shows a range such as `6–10 / 62`. The component does not calculate page ranges; you decide which links to render.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/pagination
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add pagination
```

### Manual

1. Install the dependencies and add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper):

```bash title="Terminal"
npm install @phosphor-icons/react class-variance-authority
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button).

3. Copy the source into `components/ui/pagination.tsx`:

```tsx title="components/ui/pagination.tsx"
"use client"

import {
  CaretDoubleLeftIcon,
  CaretDoubleRightIcon,
  CaretLeftIcon,
  CaretRightIcon,
  DotsThreeIcon,
} from "@phosphor-icons/react"
import type { VariantProps } from "class-variance-authority"
import type * as React from "react"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"

function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  return (
    <nav
      aria-label="pagination"
      data-slot="pagination"
      className={cn(
        "mx-auto flex w-full items-center justify-center gap-3",
        className
      )}
      {...props}
    />
  )
}

function PaginationContent({
  className,
  ...props
}: React.ComponentProps<"ul">) {
  return (
    <ul
      data-slot="pagination-content"
      className={cn("flex items-center gap-1", className)}
      {...props}
    />
  )
}

function PaginationItem({ ...props }: React.ComponentProps<"li">) {
  return <li data-slot="pagination-item" {...props} />
}

type PaginationLinkProps = {
  isActive?: boolean
} & Pick<VariantProps<typeof buttonVariants>, "size"> &
  React.ComponentProps<"a">

function PaginationLink({
  className,
  isActive,
  size = "icon-sm",
  ...props
}: PaginationLinkProps) {
  return (
    <a
      aria-current={isActive ? "page" : undefined}
      data-slot="pagination-link"
      data-active={isActive ? "" : undefined}
      className={cn(
        buttonVariants({ variant: isActive ? "secondary" : "ghost", size }),
        "text-sm tabular-nums aria-disabled:pointer-events-none data-active:font-semibold aria-disabled:opacity-50",
        className
      )}
      {...props}
    />
  )
}

function PaginationPrevious({
  className,
  text = "Previous",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label={text && text !== "Previous" ? text : "Go to previous page"}
      size={text ? "sm" : "icon-sm"}
      className={cn(text && "ps-2!", className)}
      {...props}
    >
      <CaretLeftIcon data-icon="inline-start" className="rtl:rotate-180" />
      {text && <span className="hidden sm:block">{text}</span>}
    </PaginationLink>
  )
}

function PaginationNext({
  className,
  text = "Next",
  ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
  return (
    <PaginationLink
      aria-label={text && text !== "Next" ? text : "Go to next page"}
      size={text ? "sm" : "icon-sm"}
      className={cn(text && "pe-2!", className)}
      {...props}
    >
      {text && <span className="hidden sm:block">{text}</span>}
      <CaretRightIcon data-icon="inline-end" className="rtl:rotate-180" />
    </PaginationLink>
  )
}

function PaginationFirst({
  ...props
}: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink aria-label="Go to first page" {...props}>
      <CaretDoubleLeftIcon className="rtl:rotate-180" />
    </PaginationLink>
  )
}

function PaginationLast({
  ...props
}: React.ComponentProps<typeof PaginationLink>) {
  return (
    <PaginationLink aria-label="Go to last page" {...props}>
      <CaretDoubleRightIcon className="rtl:rotate-180" />
    </PaginationLink>
  )
}

function PaginationEllipsis({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="pagination-ellipsis"
      className={cn(
        "flex size-8 items-center justify-center text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <DotsThreeIcon aria-hidden />
      <span className="sr-only">More pages</span>
    </span>
  )
}

function PaginationSummary({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="pagination-summary"
      className={cn(
        "text-sm whitespace-nowrap text-muted-foreground tabular-nums",
        className
      )}
      {...props}
    />
  )
}

function PaginationGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      role="group"
      data-slot="pagination-group"
      className={cn(
        "flex items-center gap-px overflow-hidden rounded-lg *:w-10 *:rounded-none *:border-0 *:bg-secondary *:hover:bg-secondary/80 *:focus-visible:ring-inset *:focus-visible:relative *:focus-visible:z-10",
        className
      )}
      {...props}
    />
  )
}

function PaginationInput({
  className,
  ...props
}: React.ComponentProps<"input">) {
  return (
    <input
      type="text"
      inputMode="numeric"
      data-slot="pagination-input"
      className={cn(
        "h-8 w-12 rounded-md bg-secondary text-center text-sm font-semibold text-secondary-foreground tabular-nums caret-primary outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
        className
      )}
      {...props}
    />
  )
}

export {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationFirst,
  PaginationGroup,
  PaginationInput,
  PaginationItem,
  PaginationLast,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
  PaginationSummary,
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationFirst, PaginationLast, PaginationEllipsis, PaginationSummary, PaginationGroup, PaginationInput } from "@/components/ui/pagination"
```

```tsx
<NumberedExample />
```

Links render as `a` elements, so pass `href` for real routes, or `href="#"` with an `onClick` that calls `preventDefault()` for client-side state. Mark the current page with `isActive` on `PaginationLink`. To disable an edge link, set `aria-disabled` on it; it becomes non-interactive and dimmed.

`PaginationLink` takes a `size` from the Button sizes and defaults to `icon-sm`. Set `text=""` on `PaginationPrevious` or `PaginationNext` for icon-only arrows.

## Examples

### Compact

A page number input with joined previous and next arrows in a `PaginationGroup`, clamping typed values to the valid range.

**Example**

```tsx
import * as React from "react"
import {
  Pagination,
  PaginationGroup,
  PaginationInput,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function CompactExample() {
  const [page, setPage] = React.useState(3)
  const [draft, setDraft] = React.useState("3")
  const perPage = 5
  const count = Math.ceil(orders.length / perPage)

  const change = (next: number) => {
    setPage(next)
    setDraft(String(next))
  }

  const commit = () => {
    const next = Number.parseInt(draft, 10)
    if (Number.isNaN(next)) return setDraft(String(page))
    change(Math.min(Math.max(next, 1), count))
  }

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <div className="flex items-center gap-2 text-sm text-muted-foreground">
          <label htmlFor="orders-page">Page</label>
          <PaginationInput
            id="orders-page"
            value={draft}
            onChange={(event) => setDraft(event.target.value)}
            onBlur={commit}
            onKeyDown={(event) => event.key === "Enter" && commit()}
          />
          <span className="tabular-nums">/ {count}</span>
        </div>
        <PaginationGroup>
          <PaginationPrevious text="" {...go(change, page - 1, page === 1)} />
          <PaginationNext text="" {...go(change, page + 1, page === count)} />
        </PaginationGroup>
      </Pagination>
    </Orders>
  )
}

export default function PaginationCompact() {
  return <CompactExample />
}
```

### Rows per page

A rows-per-page select, a range summary, and first, previous, next and last arrows.

**Example**

```tsx
import * as React from "react"
import {
  Pagination,
  PaginationFirst,
  PaginationGroup,
  PaginationLast,
  PaginationNext,
  PaginationPrevious,
  PaginationSummary,
} from "@/components/ui/pagination"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const places = [
  "Table 7",
  "Table 4",
  "Takeaway",
  "Table 12",
  "Pickup",
  "Garden 3",
]

const currency = new Intl.NumberFormat("tr-TR", {
  style: "currency",
  currency: "TRY",
})

const orders = Array.from({ length: 62 }, (_, index) => {
  const id = 1100 - index
  return {
    id,
    place: places[(id * 7) % places.length],
    amount: ((id * 379) % 900) + 150,
  }
})

function go(setPage: (page: number) => void, page: number, disabled: boolean) {
  return {
    href: "#",
    "aria-disabled": disabled || undefined,
    onClick: (event: React.MouseEvent) => {
      event.preventDefault()
      if (!disabled) setPage(page)
    },
  }
}

function Orders({
  page,
  perPage,
  children,
}: {
  page: number
  perPage: number
  children: React.ReactNode
}) {
  const rows = orders.slice((page - 1) * perPage, page * perPage)

  return (
    <div className="flex w-136 flex-col gap-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order</TableHead>
            <TableHead>Table</TableHead>
            <TableHead className="text-end">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="tabular-nums">#{order.id}</TableCell>
              <TableCell className="text-muted-foreground">
                {order.place}
              </TableCell>
              <TableCell className="text-end tabular-nums">
                {currency.format(order.amount)}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {children}
    </div>
  )
}

function RowsPerPageExample() {
  const [page, setPage] = React.useState(1)
  const [perPage, setPerPage] = React.useState(5)
  const count = Math.ceil(orders.length / perPage)

  return (
    <Orders page={page} perPage={perPage}>
      <Pagination className="justify-between">
        <Select
          value={String(perPage)}
          onValueChange={(value) => {
            setPerPage(Number(value))
            setPage(1)
          }}
        >
          <SelectTrigger
            size="sm"
            className="h-8 text-sm"
            aria-label="Rows per page"
          >
            <SelectValue>{(value: string) => `${value} rows`}</SelectValue>
          </SelectTrigger>
          <SelectContent side="top">
            {[5, 10, 20].map((value) => (
              <SelectItem key={value} value={String(value)}>
                {value} rows
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <PaginationSummary>
          {(page - 1) * perPage + 1}–{Math.min(page * perPage, orders.length)} /{" "}
          {orders.length}
        </PaginationSummary>
        <PaginationGroup>
          <PaginationFirst {...go(setPage, 1, page === 1)} />
          <PaginationPrevious text="" {...go(setPage, page - 1, page === 1)} />
          <PaginationNext text="" {...go(setPage, page + 1, page === count)} />
          <PaginationLast {...go(setPage, count, page === count)} />
        </PaginationGroup>
      </Pagination>
    </Orders>
  )
}

export default function PaginationRowsPerPage() {
  return <RowsPerPageExample />
}
```

### With ellipsis

A long page range shortened with ellipses on both sides of the current page.

**Example**

```tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationWithEllipsis() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">9</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            10
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">11</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">24</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

### Custom labels

Previous and next links relabelled `Newer` and `Older`.

**Example**

```tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"

export default function PaginationCustomLabels() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" text="Newer" />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" text="Older" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

### Large links

Page links using the larger `icon` button size.

**Example**

```tsx
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
} from "@/components/ui/pagination"

export default function PaginationLargeLinks() {
  return (
    <Pagination>
      <PaginationContent>
        {[1, 2, 3, 4, 5].map((page) => (
          <PaginationItem key={page}>
            <PaginationLink href="#" size="icon" isActive={page === 1}>
              {page}
            </PaginationLink>
          </PaginationItem>
        ))}
      </PaginationContent>
    </Pagination>
  )
}
```

## API Reference

- `Pagination`
- `PaginationContent`
  - `PaginationItem`
    - `PaginationPrevious`
    - `PaginationLink`
    - `PaginationEllipsis`
    - `PaginationNext`

### Pagination

The `nav` landmark that contains the pagination controls.

Also accepts the props of `React.ComponentProps<"nav">`.

### PaginationContent

The list that holds the pagination items.

Also accepts the props of `React.ComponentProps<"ul">`.

### PaginationItem

A list item wrapper for one link or ellipsis.

Also accepts the props of `React.ComponentProps<"li">`.

### PaginationLink

A page link styled as a button, highlighted when active.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | - | Marks the link as the current page with `aria-current="page"`. |

Also accepts the props of `PaginationLinkProps`.

### PaginationPrevious

A link to the previous page with a leading caret.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` | `"Previous"` | The visible label. A custom value also becomes the accessible name; an empty string renders an icon-only link. |
| `isActive` | `boolean` | - | Marks the link as the current page with `aria-current="page"`. |

Also accepts the props of `React.ComponentProps<typeof PaginationLink>`.

### PaginationNext

A link to the next page with a trailing caret.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` | `"Next"` | The visible label. A custom value also becomes the accessible name; an empty string renders an icon-only link. |
| `isActive` | `boolean` | - | Marks the link as the current page with `aria-current="page"`. |

Also accepts the props of `React.ComponentProps<typeof PaginationLink>`.

### PaginationFirst

An icon link to the first page.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | - | Marks the link as the current page with `aria-current="page"`. |

Also accepts the props of `React.ComponentProps<typeof PaginationLink>`.

### PaginationLast

An icon link to the last page.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isActive` | `boolean` | - | Marks the link as the current page with `aria-current="page"`. |

Also accepts the props of `React.ComponentProps<typeof PaginationLink>`.

### PaginationEllipsis

A placeholder that indicates skipped pages.

Also accepts the props of `React.ComponentProps<"span">`.

### PaginationSummary

Secondary text that summarizes the current range.

Also accepts the props of `React.ComponentProps<"p">`.

### PaginationGroup

Joins several links into one segmented control.

Also accepts the props of `React.ComponentProps<"div">`.

### PaginationInput

A numeric text field for jumping to a page.

Also accepts the props of `React.ComponentProps<"input">`.

## Accessibility

- `Pagination` renders a `nav` with `aria-label="pagination"`.
- The active `PaginationLink` gets `aria-current="page"`.
- Arrow links have built-in accessible names: `Go to previous page`, `Go to next page`, `Go to first page` and `Go to last page`. Custom `text` on previous or next replaces the name.
- `PaginationEllipsis` is hidden from assistive technology.
- `PaginationGroup` has `role="group"`.
- `PaginationInput` has no built-in label; associate one with `htmlFor` or `aria-label`.

## Notes

- Styling hooks: `data-slot` values `pagination`, `pagination-content`, `pagination-item`, `pagination-link`, `pagination-ellipsis`, `pagination-summary`, `pagination-group` and `pagination-input`. Active links expose `data-active`.
- Active links use the `secondary` button variant and others use `ghost`.
- Previous and next text is hidden below the `sm` breakpoint, and the carets flip in right-to-left layouts.
- `PaginationInput` uses `inputMode="numeric"` to show a number keypad on touch devices.

