# Pagination (/ui/components/react/pagination)



<Playground component="pagination" />

## Usage [#usage]

```tsx
import {
  Pagination,
  PaginationList,
  PaginationItem,
  PaginationLink,
  PaginationEllipsis,
} from '@appica/ui-react/pagination'
```

```tsx
<Pagination>
  <PaginationList>
    <PaginationItem>
      <PaginationLink href="?page=1" active>
        1
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=2">2</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=10">10</PaginationLink>
    </PaginationItem>
  </PaginationList>
</Pagination>
```

`Pagination` is a set of semantic primitives — there's no hidden state, so you build the page list yourself and decide which links to show. Every part lives under `@appica/ui-react/pagination`:

* **`Pagination`** — the `<nav aria-label="pagination">` landmark that wraps the control.
* **`PaginationList`** — the `<ul>` that lays the items out in a row.
* **`PaginationItem`** — one `<li>` wrapping a link or the ellipsis.
* **`PaginationLink`** — a page link. Mark the current page with `active` (it becomes a non-interactive `aria-current="page"` element); mark out-of-range prev/next controls with `disabled`.
* **`PaginationEllipsis`** — a decorative "…" standing in for the pages collapsed out of a long range.

The `variant` (`outline` or `soft`) and `size` (`sm`/`md`/`lg`) set on the root flow to every link through context. `PaginationLink` is built on Base UI's [`useRender`](https://base-ui.com/react/utils/use-render), so you can project each link onto your router's link — e.g. a Next.js [`Link`](https://nextjs.org/docs/app/api-reference/components/link) — without losing its styling or active state.

## Examples [#examples]

### Default [#default]

A typical truncated range: an icon previous control (disabled on the first page), the current page marked `active`, an ellipsis for the gap, the last page, and an icon next control. Prev/next are chevron icons — give each an `aria-label`, and `className="px-0"` squares the link up around the lone icon.

```tsx
import {
  Pagination,
  PaginationList,
  PaginationItem,
  PaginationLink,
  PaginationEllipsis,
} from '@appica/ui-react/pagination'
import { ChevronLeft, ChevronRight } from '@appica/icons-react'

export default function PaginationDefault() {
  return (
    <Pagination>
      <PaginationList>
        <PaginationItem>
          <PaginationLink href="#!" aria-label="Go to previous page" className="px-0" disabled>
            <ChevronLeft />
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#!" active>
            1
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#!">2</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#!">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#!">10</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#!" aria-label="Go to next page" className="px-0">
            <ChevronRight />
          </PaginationLink>
        </PaginationItem>
      </PaginationList>
    </Pagination>
  )
}
```

### Variants [#variants]

`outline` (the default) draws a bordered button for each page; `soft` drops the borders for a lighter, fill-on-hover look. The `active` page is filled in both.

```tsx
import { Pagination, PaginationList, PaginationItem, PaginationLink } from '@appica/ui-react/pagination'

export default function PaginationVariants() {
  return (
    <div className="flex flex-col items-center gap-6">
      {(['outline', 'soft'] as const).map((variant) => (
        <Pagination key={variant} variant={variant}>
          <PaginationList>
            <PaginationItem>
              <PaginationLink href="#!" active>
                1
              </PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#!">2</PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#!">3</PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#!">4</PaginationLink>
            </PaginationItem>
          </PaginationList>
        </Pagination>
      ))}
    </div>
  )
}
```

### Sizes [#sizes]

Three sizes — `sm`, `md` (default), and `lg` — scale each link's height, minimum width, and text together.

```tsx
import { Pagination, PaginationList, PaginationItem, PaginationLink } from '@appica/ui-react/pagination'

export default function PaginationSizes() {
  return (
    <div className="flex flex-col items-center gap-6">
      {(['sm', 'md', 'lg'] as const).map((size) => (
        <Pagination key={size} size={size}>
          <PaginationList>
            <PaginationItem>
              <PaginationLink href="#!" active>
                1
              </PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#!">2</PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#!">3</PaginationLink>
            </PaginationItem>
          </PaginationList>
        </Pagination>
      ))}
    </div>
  )
}
```

### With first & last [#with-first--last]

For long ranges, flank the prev/next controls with first and last jump buttons — double-chevron [icons](/ui/icons) that skip straight to the ends. Each is icon-only, so it needs an `aria-label` and `className="px-0"`.

```tsx
import { Pagination, PaginationList, PaginationItem, PaginationLink } from '@appica/ui-react/pagination'
import { ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight } from '@appica/icons-react'

export default function PaginationFirstLast() {
  return (
    <div className="max-w-full scrollbar-none overflow-auto [&::-webkit-scrollbar]:hidden">
      <Pagination>
        <PaginationList>
          <PaginationItem>
            <PaginationLink href="#!" aria-label="Go to first page" className="px-0">
              <ChevronsLeft />
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!" aria-label="Go to previous page" className="px-0">
              <ChevronLeft />
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!">1</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!">2</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!" active>
              3
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!">4</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!">5</PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!" aria-label="Go to next page" className="px-0">
              <ChevronRight />
            </PaginationLink>
          </PaginationItem>
          <PaginationItem>
            <PaginationLink href="#!" aria-label="Go to last page" className="px-0">
              <ChevronsRight />
            </PaginationLink>
          </PaginationItem>
        </PaginationList>
      </Pagination>
    </div>
  )
}
```

### Controlled [#controlled]

`Pagination` holds no state of its own, so wire it to your own page state. Here the current page lives in `useState`, each link's `onClick` updates it instead of navigating, the active page reflects state, and prev/next disable at the bounds.

```tsx
'use client'

import * as React from 'react'
import { Pagination, PaginationList, PaginationItem, PaginationLink } from '@appica/ui-react/pagination'
import { ChevronLeft, ChevronRight } from '@appica/icons-react'

const TOTAL = 5

export default function PaginationControlled() {
  const [page, setPage] = React.useState(1)

  const go = (target: number) => (event: React.MouseEvent) => {
    event.preventDefault()
    setPage(Math.min(Math.max(target, 1), TOTAL))
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <p className="text-foreground-muted text-sm">
        Page {page} of {TOTAL}
      </p>
      <Pagination>
        <PaginationList>
          <PaginationItem>
            <PaginationLink
              href="#!"
              aria-label="Go to previous page"
              className="px-0"
              disabled={page === 1}
              onClick={go(page - 1)}
            >
              <ChevronLeft />
            </PaginationLink>
          </PaginationItem>
          {Array.from({ length: TOTAL }, (_, index) => index + 1).map((p) => (
            <PaginationItem key={p}>
              <PaginationLink href="#!" active={p === page} aria-label={`Go to page ${p}`} onClick={go(p)}>
                {p}
              </PaginationLink>
            </PaginationItem>
          ))}
          <PaginationItem>
            <PaginationLink
              href="#!"
              aria-label="Go to next page"
              className="px-0"
              disabled={page === TOTAL}
              onClick={go(page + 1)}
            >
              <ChevronRight />
            </PaginationLink>
          </PaginationItem>
        </PaginationList>
      </Pagination>
    </div>
  )
}
```

### Router links [#router-links]

For client-side routing, project each `PaginationLink` onto your framework's link via `render` — here a Next.js [`Link`](https://nextjs.org/docs/app/api-reference/components/link). The link keeps its styling and active handling while the router owns navigation; put the `href` on the `Link` and any class overrides on the `PaginationLink` wrapper. Mark the current page `active` (no `Link` needed — it's non-interactive).

```tsx
import Link from 'next/link'
import { Pagination, PaginationList, PaginationItem, PaginationLink } from '@appica/ui-react/pagination'
import { ChevronLeft, ChevronRight } from '@appica/icons-react'

export default function PaginationRouter() {
  return (
    <Pagination>
      <PaginationList>
        <PaginationItem>
          <PaginationLink aria-label="Go to previous page" className="px-0" render={<Link href="?page=1" />}>
            <ChevronLeft />
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink render={<Link href="?page=1" />}>1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink active>2</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink render={<Link href="?page=3" />}>3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink aria-label="Go to next page" className="px-0" render={<Link href="?page=3" />}>
            <ChevronRight />
          </PaginationLink>
        </PaginationItem>
      </PaginationList>
    </Pagination>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The page list flows from the right, and the previous/next controls swap sides — point their chevrons the other way so they still read as "back" and "forward". For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="pagination" />

## API reference [#api-reference]

Pagination is a set of styled HTML elements. Each part forwards every remaining prop to its underlying element (`nav`, `ul`, `li`, `a`, `span`), so any native attribute works here — the tables below list the Appica additions and the most common props.

### Pagination [#pagination]

The navigation landmark. Renders a `<nav role="navigation" aria-label="pagination">` and provides the `variant`/`size` context.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth> | Default     | <ColMinWidth width="220">Description</ColMinWidth> |
| ----------- | ------------------------------------------- | ----------- | -------------------------------------------------- |
| `variant`   | <Code>'outline' \| 'soft'</Code>            | `'outline'` | Link style, shared with every link via context.    |
| `size`      | <Code>'sm' \| 'md' \| 'lg'</Code>           | `'md'`      | Link sizing, shared via context.                   |
| `children`  | `ReactNode`                                 | —           | A single `PaginationList`.                         |
| `className` | `string`                                    | —           | Extra classes, merged via `tailwind-merge`.        |

### PaginationList [#paginationlist]

The list of items. Renders a `<ul>` laying the items out in a row.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | `PaginationItem`s.                          |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### PaginationItem [#paginationitem]

One item's `<li>` wrapper.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | A `PaginationLink` or `PaginationEllipsis`. |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### PaginationLink [#paginationlink]

A page link. Built on Base UI's [`useRender`](https://base-ui.com/react/utils/use-render); renders an `<a>` unless you project it onto another element with `render`. Inherits `variant`/`size` from the root.

| Prop        | <ColMinWidth width="220">Type</ColMinWidth>                 | Default | <ColMinWidth width="220">Description</ColMinWidth>                                          |
| ----------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
| `active`    | `boolean`                                                   | `false` | Mark the current page. Renders a non-interactive, filled link with `aria-current="page"`.   |
| `disabled`  | `boolean`                                                   | `false` | Make the link non-interactive and dimmed (`aria-disabled`); used for out-of-range controls. |
| `href`      | `string`                                                    | —       | Navigation target (when rendered as an `<a>`).                                              |
| `render`    | <Code>ReactElement \| (props, state) => ReactElement</Code> | —       | Render as your own element (e.g. a router `Link`) or a `<button>`.                          |
| `className` | `string`                                                    | —       | Extra classes, merged via `tailwind-merge`.                                                 |

### PaginationEllipsis [#paginationellipsis]

A "…" placeholder for collapsed pages. Decorative (`aria-hidden`), with an `sr-only` "More pages" label.

| Prop        | Type        | Default       | Description                                 |
| ----------- | ----------- | ------------- | ------------------------------------------- |
| `children`  | `ReactNode` | ellipsis dots | Override the placeholder mark.              |
| `className` | `string`    | —             | Extra classes, merged via `tailwind-merge`. |

## Accessibility [#accessibility]

* `Pagination` renders a `<nav>` with `aria-label="pagination"`, so it's exposed as a named navigation landmark.
* The current page (`active`) carries `aria-current="page"` and is non-interactive, so it isn't announced as a destination.
* `disabled` links set `aria-disabled` and drop out of the tab order — the right way to express an out-of-range Previous/Next control.
* Controls without visible text — icon-only prev/next, for instance — need an `aria-label`.
* The ellipsis is `aria-hidden` and keeps an `sr-only` "More pages" label for context.
