Pagination
Pagination lets users navigate data split across multiple pages.
Installation
Install the pagination package:
npm install @zag-js/pagination @zag-js/react # or yarn add @zag-js/pagination @zag-js/react
npm install @zag-js/pagination @zag-js/solid # or yarn add @zag-js/pagination @zag-js/solid
npm install @zag-js/pagination @zag-js/vue # or yarn add @zag-js/pagination @zag-js/vue
npm install @zag-js/pagination @zag-js/svelte # or yarn add @zag-js/pagination @zag-js/svelte
Anatomy
To set up the pagination correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the pagination package:
import * as pagination from "@zag-js/pagination"
The pagination package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
import * as pagination from "@zag-js/pagination" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" import { data } from "./data" function Pagination() { const service = useMachine(pagination.machine, { id: useId(), count: data.length, }) const api = pagination.connect(service, normalizeProps) return ( <div> {api.totalPages > 1 && ( <nav {...api.getRootProps()}> <ul> <li> <a href="#previous" {...api.getPrevTriggerProps()}> Previous <span className="visually-hidden">Page</span> </a> </li> {api.pages.map((page, i) => { if (page.type === "page") return ( <li key={page.value}> <a href={`#${page.value}`} {...api.getItemProps(page)}> {page.value} </a> </li> ) else return ( <li key={`ellipsis-${i}`}> <span {...api.getEllipsisProps({ index: i })}>…</span> </li> ) })} <li> <a href="#next" {...api.getNextTriggerProps()}> Next <span className="visually-hidden">Page</span> </a> </li> </ul> </nav> )} </div> ) }
import * as pagination from "@zag-js/pagination" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" import { data } from "./data" function Pagination() { const service = useMachine(pagination.machine, { id: createUniqueId(), count: data.length, }) const api = createMemo(() => pagination.connect(service, normalizeProps)) return ( <div> {api().totalPages > 1 && ( <nav {...api().getRootProps()}> <ul> <li> <a href="#previous" {...api().getPrevTriggerProps()}> Previous <span class="visually-hidden">Page</span> </a> </li> <For each={api().pages}> {(page, i) => { if (page.type === "page") return ( <li> <a href={`#${page.value}`} {...api().getItemProps(page)}> {page.value} </a> </li> ) return ( <li> <span {...api().getEllipsisProps({ index: i() })}> … </span> </li> ) }} </For> <li> <a href="#next" {...api().getNextTriggerProps()}> Next <span class="visually-hidden">Page</span> </a> </li> </ul> </nav> )} </div> ) }
<script setup> import * as pagination from "@zag-js/pagination" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" import { data } from "./data" const service = useMachine(pagination.machine, { id: "1", count: data.length, }) const api = computed(() => pagination.connect(service, normalizeProps)) </script> <template> <nav v-if="api.totalPages > 1" v-bind="api.getRootProps()"> <ul> <li> <a href="#previous" v-bind="api.getPrevTriggerProps()"> Previous <span class="visually-hidden">Page</span> </a> </li> <li v-for="(page, i) in api.pages" :key="page.type === 'page' ? page.value : `ellipsis-${i}`" > <span v-if="page.type === 'page'"> <a :href="`#${page.value}`" v-bind="api.getItemProps(page)"> {{page.value}} </a></span > <span v-else> <span v-bind="api.getEllipsisProps({ index: i })">…</span> </span> </li> <li> <a href="#next" v-bind="api.getNextTriggerProps()"> Next <span class="visually-hidden">Page</span> </a> </li> </ul> </nav> </template>
<script lang="ts"> import * as pagination from "@zag-js/pagination" import { useMachine, normalizeProps } from "@zag-js/svelte" import { data } from "./data" const id = $props.id() const service = useMachine(pagination.machine, { id, count: data.length, }) const api = $derived(pagination.connect(service, normalizeProps)) </script> <div> {#if api.totalPages > 1} <nav {...api.getRootProps()}> <ul> <li> <a href="#previous" {...api.getPrevTriggerProps()}> Previous <span class="visually-hidden">Page</span> </a> </li> {#each api.pages as page, i} {#if page.type === "page"} <li> <a href={`#${page.value}`} {...api.getItemProps(page)}> {page.value} </a> </li> {:else} <li> <span {...api.getEllipsisProps({ index: i })}>…</span> </li> {/if} {/each} <li> <a href="#next" {...api.getNextTriggerProps()}> Next <span class="visually-hidden">Page</span> </a> </li> </ul> </nav> {/if} </div>
const api = pagination.connect(service) // You can slice the data, to get data for current page const currentPageData = data.slice(api.pageRange.start, api.pageRange.end) api.page // => 1 api.setPage(3) // page => 3 api.previousPage // => 2 api.nextPage // => 4 api.pages /* [ { "type": "page", "value": 1, }, { "type": "page", "value": 2, }, { "type": "ellipsis", }, { "type": "page", "value": 3, }, { "type": "page", "value": 4, }, ] */ api.pageRange // => { start: 4, end: 13 }
Controlled page
Use page and onPageChange to control the current page externally.
const service = useMachine(pagination.machine, { count: 500, pageSize: 20, page, onPageChange(details) { setPage(details.page) }, })
Controlled page size
Use pageSize and onPageSizeChange to control page size.
const service = useMachine(pagination.machine, { count: 500, pageSize, onPageSizeChange(details) { setPageSize(details.pageSize) }, })
Initial page and page size
Use defaultPage and defaultPageSize for uncontrolled initial values.
const service = useMachine(pagination.machine, { count: 500, defaultPage: 3, defaultPageSize: 20, })
Link-based pagination
Set type: "link" and provide getPageUrl to render real navigation links.
const service = useMachine(pagination.machine, { count: 500, type: "link", getPageUrl(details) { return `/products?page=${details.page}&pageSize=${details.pageSize}` }, })
Customizing visible page ranges
Use siblingCount and boundaryCount to control how many page items are shown.
const service = useMachine(pagination.machine, { count: 500, siblingCount: 2, boundaryCount: 2, })
Customizing accessibility labels
Use translations to customize trigger and page labels.
const service = useMachine(pagination.machine, { count: 500, translations: { rootLabel: "Results pages", prevTriggerLabel: "Previous page", nextTriggerLabel: "Next page", itemLabel: ({ page, totalPages }) => `Page ${page} of ${totalPages}`, }, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
[data-part="root"] { /* styles for the pagination's root */ } [data-part="item"] { /* styles for the pagination's items */ } [data-part="ellipsis"] { /* styles for the pagination's ellipsis */ } [data-part="prev-trigger"] { /* styles for the pagination's previous page trigger */ } [data-part="next-trigger"] { /* styles for the pagination's next page trigger */ } /* We add a data-disabled attribute to the prev/next items when on the first/last page */ [data-part="prev-trigger"][data-disabled] { /* styles for the pagination's previous page trigger when on first page */ } [data-part="next-trigger"][data-disabled] { /* styles for the pagination's next page trigger when on first page */ }
Methods and Properties
Machine Context
The pagination machine exposes the following context properties:
idsPartial<{ root: string; ellipsis: (index: number) => string; firstTrigger: string; prevTrigger: string; nextTrigger: string; lastTrigger: string; item: (page: number) => string; }> | undefinedThe ids of the elements in the accordion. Useful for composition.translationsIntlTranslations | undefinedSpecifies the localized strings that identifies the accessibility elements and their statescountnumber | undefinedTotal number of data itemspageSizenumber | undefinedThe controlled number of data items per pagedefaultPageSizenumber | undefinedThe initial number of data items per page when rendered. Use when you don't need to control the page size of the pagination.siblingCountnumber | undefinedNumber of pages to show beside active pageboundaryCountnumber | undefinedNumber of pages to show at the beginning and endpagenumber | undefinedThe controlled active pagedefaultPagenumber | undefinedThe initial active page when rendered. Use when you don't need to control the active page of the pagination.onPageChange((details: PageChangeDetails) => void) | undefinedCalled when the page number is changedonPageSizeChange((details: PageSizeChangeDetails) => void) | undefinedCalled when the page size is changedtype"button" | "link" | undefinedThe type of the trigger elementgetPageUrl((details: PageUrlDetails) => string) | undefinedFunction to generate href attributes for pagination links. Only used when `type` is set to "link".dir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The pagination api exposes the following methods:
pagenumberThe current page.countnumberThe total number of data items.pageSizenumberThe number of data items per page.totalPagesnumberThe total number of pages.pagesPagesThe page range. Represented as an array of page numbers (including ellipsis)previousPagenumber | nullThe previous page.nextPagenumber | nullThe next page.pageRangePageRangeThe page range. Represented as an object with `start` and `end` properties.slice<V>(data: V[]) => V[]Function to slice an array of data based on the current page.setPageSize(size: number) => voidFunction to set the page size.setPage(page: number) => voidFunction to set the current page.goToNextPageVoidFunctionFunction to go to the next page.goToPrevPageVoidFunctionFunction to go to the previous page.goToFirstPageVoidFunctionFunction to go to the first page.goToLastPageVoidFunctionFunction to go to the last page.