Skip to main content
Pagination lets users navigate data split across multiple pages. It generates a list of page items including ellipsis markers and previous/next triggers, and exposes a pageRange to slice your data.

Installation

npm install @zag-js/pagination @zag-js/react

Usage

import { useMachine, normalizeProps } from "@zag-js/react"
import * as pagination from "@zag-js/pagination"
import { useId } from "react"

const data = Array.from({ length: 500 }, (_, i) => ({ id: i + 1 }))

export function Pagination() {
  const service = useMachine(pagination.machine, {
    id: useId(),
    count: data.length,
    pageSize: 20,
  })
  const api = pagination.connect(service, normalizeProps)

  const currentItems = data.slice(api.pageRange.start, api.pageRange.end)

  return (
    <div>
      <p>{currentItems.length} items on this page</p>
      <nav {...api.getRootProps()}>
        <button {...api.getPrevTriggerProps()}>Previous</button>
        {api.pages.map((page, i) =>
          page.type === "page" ? (
            <button key={page.value} {...api.getItemProps(page)}>
              {page.value}
            </button>
          ) : (
            <span key={`ellipsis-${i}`} {...api.getEllipsisProps({ index: i })}>
              &#8230;
            </span>
          )
        )}
        <button {...api.getNextTriggerProps()}>Next</button>
      </nav>
    </div>
  )
}

Controlled page

Use page and onPageChange to control the current page from outside the machine.
const [page, setPage] = useState(1)

const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  pageSize: 20,
  page,
  onPageChange(details) {
    setPage(details.page)
  },
})

Controlled page size

Use pageSize and onPageSizeChange to control the page size.
const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  pageSize,
  onPageSizeChange(details) {
    setPageSize(details.pageSize)
  },
})

Default page and page size

For uncontrolled usage, set initial values with defaultPage and defaultPageSize.
const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  defaultPage: 3,
  defaultPageSize: 20,
})
Set type: "link" and provide getPageUrl to render real anchor navigation links.
const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  type: "link",
  getPageUrl(details) {
    return `/products?page=${details.page}&pageSize=${details.pageSize}`
  },
})

Customizing visible page range

Use siblingCount (pages around the current page) and boundaryCount (pages at the start/end) to control how many page items appear.
const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  siblingCount: 2,
  boundaryCount: 2,
})

Accessibility labels

Use translations to customize ARIA labels on triggers and page items.
const service = useMachine(pagination.machine, {
  id: useId(),
  count: 500,
  translations: {
    rootLabel: "Results pages",
    prevTriggerLabel: "Previous page",
    nextTriggerLabel: "Next page",
    itemLabel: ({ page, totalPages }) => `Page ${page} of ${totalPages}`,
  },
})

API reference

count
number
required
The total number of items to paginate.
pageSize
number
The number of items per page. Controlled. Use with onPageSizeChange.
defaultPageSize
number
The initial page size (uncontrolled). Defaults to 10.
page
number
The controlled current page number (1-indexed).
defaultPage
number
The initial page (uncontrolled). Defaults to 1.
siblingCount
number
The number of page items to show on each side of the current page. Defaults to 1.
boundaryCount
number
The number of pages to show at the start and end of the page list. Defaults to 1.
type
"button" | "link"
Whether to render page items as buttons or anchor links. Defaults to "button".
getPageUrl
(details: { page: number, pageSize: number }) => string
A function that returns the URL for a given page. Required when type is "link".
onPageChange
(details: { page: number, pageSize: number }) => void
Callback fired when the active page changes.
onPageSizeChange
(details: { pageSize: number, page: number }) => void
Callback fired when the page size changes.
translations
IntlTranslations
Localized labels for accessibility attributes.

Styling

Each part has a data-part attribute. The previous and next triggers receive data-disabled when on the first or last page.
[data-part="root"] { /* pagination container */ }
[data-part="item"] { /* individual page button */ }
[data-part="ellipsis"] { /* ellipsis element */ }
[data-part="prev-trigger"] { /* previous page button */ }
[data-part="next-trigger"] { /* next page button */ }

[data-part="prev-trigger"][data-disabled] {
  /* styles when on the first page */
}

[data-part="next-trigger"][data-disabled] {
  /* styles when on the last page */
}

[data-part="item"][data-selected] {
  /* styles for the currently active page item */
}

Build docs developers (and LLMs) love