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
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 })}>
…
</span>
)
)}
<button {...api.getNextTriggerProps()}>Next</button>
</nav>
</div>
)
}
<script setup>
import { useMachine, normalizeProps } from "@zag-js/vue"
import * as pagination from "@zag-js/pagination"
import { useId, computed } from "vue"
const data = Array.from({ length: 500 }, (_, i) => ({ id: i + 1 }))
const service = useMachine(pagination.machine, {
id: useId(),
count: data.length,
pageSize: 20,
})
const api = computed(() => pagination.connect(service, normalizeProps))
</script>
<template>
<nav v-bind="api.getRootProps()">
<button v-bind="api.getPrevTriggerProps()">Previous</button>
<template v-for="(page, i) in api.pages" :key="i">
<button v-if="page.type === 'page'" v-bind="api.getItemProps(page)">
{{ page.value }}
</button>
<span v-else v-bind="api.getEllipsisProps({ index: i })">…</span>
</template>
<button v-bind="api.getNextTriggerProps()">Next</button>
</nav>
</template>
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
The total number of items to paginate.
The number of items per page. Controlled. Use with onPageSizeChange.
The initial page size (uncontrolled). Defaults to 10.
The controlled current page number (1-indexed).
The initial page (uncontrolled). Defaults to 1.
The number of page items to show on each side of the current page. Defaults to 1.
The number of pages to show at the start and end of the page list. Defaults to 1.
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.
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 */
}