# Pagination

Pagination with page navigation, next and previous links.

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

export default function PaginationDemo() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href="#" />
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href="#" />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation). Pagination also needs the
   [Button](https://carbon.style/docs/components/button), which it imports `buttonStyles` from, and
   `interop.css`, which sizes the chevrons and the ellipsis icon.

3. Copy and paste the following code into your project.

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

   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"

   import { buttonStyles, type ButtonSize } from "@/components/ui/button"
   import { breakpoints } from "@/theme/breakpoints.stylex"
   import { space } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     nav: {
       display: "flex",
       justifyContent: "center",
       width: "100%",
       marginInline: "auto",
     },
     content: {
       display: "flex",
       alignItems: "center",
       gap: space.s0_5,
       listStyleType: "none",
     },
     ellipsis: {
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       width: space.s8,
       height: space.s8,
       "--carbon-icon-size": space.s4,
     },
     text: {
       display: { default: "none", [breakpoints.sm]: "block" },
     },
   })

   const paginationStyles = {
     nav: styles.nav,
     content: styles.content,
     ellipsis: styles.ellipsis,
     text: styles.text,
   }

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   function Pagination({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"nav"> & Overrides) {
     const sx = stylex.props(styles.nav, xstyle)
     return useRender({
       defaultTagName: "nav",
       render,
       ref,
       props: {
         "aria-label": "pagination",
         "data-slot": "pagination",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function PaginationContent({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"ul"> & Overrides) {
     const sx = stylex.props(styles.content, xstyle)
     return useRender({
       defaultTagName: "ul",
       render,
       ref,
       props: {
         "data-slot": "pagination-content",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function PaginationItem({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"li"> & Overrides) {
     const sx = stylex.props(xstyle)
     return useRender({
       defaultTagName: "li",
       render,
       ref,
       props: {
         "data-slot": "pagination-item",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   type PaginationLinkProps = useRender.ComponentProps<"a", { active: boolean }> & {
     isActive?: boolean
     size?: ButtonSize
     xstyle?: StyleXStyles
   }

   function PaginationLink({
     isActive = false,
     size = "icon",
     xstyle,
     className,
     render,
     ...props
   }: PaginationLinkProps) {
     const variant = isActive ? "outline" : "ghost"
     const sx = stylex.props(
       buttonStyles.base,
       buttonStyles.pressable,
       buttonStyles.variants[variant],
       buttonStyles.sizes[size],
       xstyle,
     )
     return useRender({
       defaultTagName: "a",
       render,
       state: { active: isActive },
       stateAttributesMapping: { active: (active) => (active ? { "data-active": "true" } : null) },
       props: {
         "aria-current": isActive ? "page" : undefined,
         "data-slot": "pagination-link",
         "data-variant": variant,
         "data-size": size,
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
       },
     })
   }

   type PaginationDirectionProps = PaginationLinkProps & {
     text?: string
   }

   function PaginationPrevious({
     text = "Previous",
     size = "default",
     xstyle,
     ...props
   }: PaginationDirectionProps) {
     return (
       <PaginationLink
         aria-label="Go to previous page"
         size={size}
         xstyle={[buttonStyles.iconStart[size], xstyle]}
         {...props}
       >
         <ChevronLeft />
         <span data-slot="pagination-previous-text" {...stylex.props(styles.text)}>
           {text}
         </span>
       </PaginationLink>
     )
   }

   function PaginationNext({
     text = "Next",
     size = "default",
     xstyle,
     ...props
   }: PaginationDirectionProps) {
     return (
       <PaginationLink
         aria-label="Go to next page"
         size={size}
         xstyle={[buttonStyles.iconEnd[size], xstyle]}
         {...props}
       >
         <span data-slot="pagination-next-text" {...stylex.props(styles.text)}>
           {text}
         </span>
         <ChevronRight />
       </PaginationLink>
     )
   }

   function PaginationEllipsis({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"span"> & Overrides) {
     const sx = stylex.props(styles.ellipsis, xstyle)
     return useRender({
       defaultTagName: "span",
       render,
       ref,
       props: {
         "aria-hidden": "true",
         "data-slot": "pagination-ellipsis",
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
         children: (
           <>
             <MoreHorizontal />
           </>
         ),
       },
     })
   }

   export {
     Pagination,
     PaginationContent,
     PaginationEllipsis,
     PaginationItem,
     PaginationLink,
     PaginationNext,
     PaginationPrevious,
     paginationStyles,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="#" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#" isActive>
        2
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="#">3</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="#" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
```

## Composition

```text
Pagination
└── PaginationContent
    ├── PaginationItem
    │   └── PaginationPrevious
    ├── PaginationItem
    │   └── PaginationLink
    ├── PaginationItem
    │   └── PaginationEllipsis
    └── PaginationItem
        └── PaginationNext
```

## Simple

Page numbers only. Pass `isActive` to the current page.

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

export default function PaginationSimple() {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationLink href="#">1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#" isActive>
            2
          </PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">3</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">4</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink href="#">5</PaginationLink>
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

## Icons only

Use `PaginationPrevious` and `PaginationNext` without page numbers, next to a
rows-per-page control.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldLabel } from "@/components/ui/field"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  row: { width: "100%" },
  field: { width: "fit-content" },
  trigger: { width: "5rem" },
  pagination: { width: "auto" },
})

const rows = ["10", "25", "50", "100"]

export default function PaginationIconsOnly() {
  return (
    <HStack gap={4} align="center" justify="between" xstyle={styles.row}>
      <Field orientation="horizontal" xstyle={styles.field}>
        <FieldLabel>Rows per page</FieldLabel>
        <Select defaultValue="25">
          <SelectTrigger xstyle={styles.trigger} id="select-rows-per-page">
            <SelectValue />
          </SelectTrigger>
          <SelectContent align="start">
            <SelectGroup>
              {rows.map((value) => (
                <SelectItem key={value} value={value}>
                  {value}
                </SelectItem>
              ))}
            </SelectGroup>
          </SelectContent>
        </Select>
      </Field>
      <Pagination xstyle={styles.pagination}>
        <PaginationContent>
          <PaginationItem>
            <PaginationPrevious href="#" />
          </PaginationItem>
          <PaginationItem>
            <PaginationNext href="#" />
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </HStack>
  )
}
```

> **Note**
>
> `Pagination` fills the width of its container and centres the links. Beside
> another control, set `width: "auto"` through `xstyle` so the nav shrinks to
> its content.

## Link component

Pass a router link through `render` on `PaginationLink`. `PaginationLink` then
applies the button styles and the props to your component instead of an `a`.

```tsx
import { Link } from "@tanstack/react-router"
```

```tsx
<PaginationLink render={<Link to="/posts" search={{ page: 2 }} />} isActive>
  2
</PaginationLink>
```

> **Warning**
>
> Do not render a `PaginationLink` from `Button`. Base UI's `Button` puts
> `role="button"` on the element it renders, which takes the link role away.
> Middle click and open-in-new-tab stop working, and a screen reader announces a
> button. `PaginationLink` is an `a` styled with `buttonStyles` for this reason.

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part takes `xstyle` and `className`.

### Pagination

The `nav` landmark. It renders `aria-label="pagination"`; pass your own when
a page has more than one.

### PaginationLink

Renders an `a` and accepts its props. The current page takes the `outline`
button variant and `aria-current="page"`; every other page is `ghost`.

| Prop       | Type                                                                                 | Default  |
| ---------- | ------------------------------------------------------------------------------------ | -------- |
| `isActive` | `boolean`                                                                            | `false`  |
| `size`     | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"icon"` |
| `render`   | `React.ReactElement`                                                                 | `<a />`  |

### PaginationPrevious and PaginationNext

A `PaginationLink` with a chevron and a word. The word is hidden below `40rem`,
and the `aria-label` reads "Go to previous page" or "Go to next page" whatever
`text` says.

| Prop   | Type                                                                                 | Default                 |
| ------ | ------------------------------------------------------------------------------------ | ----------------------- |
| `text` | `string`                                                                             | `"Previous"` / `"Next"` |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"`             |

### PaginationEllipsis

Renders a `span` marked `aria-hidden`. It is the size of an `icon` link, so
the row keeps its rhythm.

### paginationStyles

The style map behind the component. Spread it onto any element. A link's styles
are `buttonStyles` from the Button page.

| Key        | Description                                     |
| ---------- | ----------------------------------------------- |
| `nav`      | The landmark, full width, its links centred     |
| `content`  | The list row and the gap between links          |
| `ellipsis` | The ellipsis box and its icon size              |
| `text`     | The word beside a chevron, hidden below `40rem` |

### Data attributes

| Attribute                              | On               | Effect                                    |
| -------------------------------------- | ---------------- | ----------------------------------------- |
| `data-slot="pagination"`               | the nav          | Targets the landmark from a parent        |
| `data-slot="pagination-content"`       | the list         | Targets the row                           |
| `data-slot="pagination-item"`          | an item          | Targets one list item                     |
| `data-slot="pagination-link"`          | a link           | Present on whatever `render` produces     |
| `data-active`                          | the current link | Set when `isActive` is true               |
| `data-variant`, `data-size`            | a link           | `outline` or `ghost`, and the `size` prop |
| `data-slot="pagination-previous-text"` | the word         | The text inside `PaginationPrevious`      |
| `data-slot="pagination-next-text"`     | the word         | The text inside `PaginationNext`          |
| `data-slot="pagination-ellipsis"`      | the ellipsis     | Also `aria-hidden`                        |

A link, the previous and next controls and the ellipsis carry the
`carbon-icons` class and set `--carbon-icon-size`. `interop.css` reads both to
size the icon inside each.
