ComponentsNavigation
Pagination
Move between pages of results. Use the ready-made Paginator for state-driven lists with automatic ellipses, a compact "Page x of y" mode and two sizes, or compose link-based pagination where every page has its own URL. Exports the getPageRange helper behind the ellipsis logic.
Installation
pnpm dlx shadcn@latest add @desyne/paginationThe CLI installs dependencies and any other components this one uses.
Usage
import { Paginator } from "@/components/ui/pagination";const [page, setPage] = useState(1);
<Paginator page={page} pageCount={20} onPageChange={setPage} />;For link-based pagination, where each page has its own URL, compose the parts:
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="?page=1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2" isActive>2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=3" />
</PaginationItem>
</PaginationContent>
</Pagination>When to use
Paginator— client-side state: tables, lists and search results where the page lives in React state and URLs don't need to change.- Link-based parts — server-rendered or routed lists where each page should be linkable, bookmarkable and crawlable (
?page=3). - Simple mode — narrow spaces like card footers and mobile layouts, or very large page counts where numbers don't help.
- Consider infinite scrolling or a "Load more" Button for feeds, where users browse rather than look for a specific page.
Anatomy
<Pagination> {/* <nav aria-label="pagination"> */}
<PaginationContent> {/* <ul> */}
<PaginationItem> {/* <li> */}
<PaginationPrevious />
</PaginationItem>
<PaginationItem>
<PaginationLink /> {/* page number */}
</PaginationItem>
<PaginationItem>
<PaginationEllipsis /> {/* skipped pages */}
</PaginationItem>
<PaginationItem>
<PaginationNext />
</PaginationItem>
</PaginationContent>
</Pagination>
<Paginator /> {/* all of the above, with buttons, in one component */}| Part | Renders | Notes |
|---|---|---|
Pagination | <nav aria-label="pagination"> | Landmark. Centered, full width by default. |
PaginationContent | <ul> | Row of items with gap-1. |
PaginationItem | <li> | Wraps one link, button or ellipsis. |
PaginationLink | React Aria Link | A page link styled with buttonVariants: ghost normally, outline with a primary border when isActive (which also sets aria-current="page"). |
PaginationPrevious / PaginationNext | PaginationLink | Chevron plus "Previous" / "Next" text. The text is hidden below the sm breakpoint. |
PaginationEllipsis | <span aria-hidden> | "…" icon for skipped pages. |
Paginator | Pagination with buttons | Previous and next icon buttons, page-number buttons from getPageRange(), or "Page x of y" in simple mode. |
Examples
Sizes
md (32px, default) and sm (28px) for tables and dense toolbars.
Simple
simple replaces the page numbers with "Page x of y" between the arrows. It takes a fixed amount of space regardless of the page count.
Siblings
siblings sets how many pages are shown on each side of the current page (default 1). The first and last pages are always shown, and a gap becomes an ellipsis.
Few pages
When every page fits (pageCount up to siblings * 2 + 5, so 7 by default), all numbers are shown with no ellipsis. Previous is disabled on the first page and Next on the last.
Link-based
Compose PaginationLink, PaginationPrevious, PaginationNext and PaginationEllipsis when each page is a URL. Mark the current page with isActive.
Link sizes
Link parts accept size="sm" or size="md" ("default" and "icon" are accepted as aliases for md).
With a router and getPageRange
Build the page list with getPageRange(page, pageCount, siblings?), which returns numbers and "ellipsis" markers, and disable Previous and Next at the ends with isDisabled. Wrap the app in React Aria's RouterProvider so the links use client-side navigation. The preview uses a local stand-in for the router.
With the Next.js App Router, read the page from the URL:
import { InvoicePagination } from "./invoice-pagination";
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
const page = Number((await searchParams).page ?? 1);
// …fetch the rows for `page`
return <InvoicePagination page={page} pageCount={24} />;
}InvoicePagination is a client component that renders the parts as in the preview, with an href of ?page=N for each page. Your app's RouterProvider passes router.push as navigate.
getPageRange(5, 10); // [1, "ellipsis", 4, 5, 6, "ellipsis", 10]
getPageRange(2, 10); // [1, 2, 3, "ellipsis", 10]
getPageRange(2, 6); // [1, 2, 3, 4, 5, 6]Recipes
Table footer
A rows-per-page Select, a range summary and a small paginator under a Table. Changing the page size resets to page 1. Pass className="mx-0 w-auto" to stop the paginator from centering itself.
| Invoice | Customer | Status | Amount |
|---|---|---|---|
| INV-1001 | Acme Corp | Paid | $480.00 |
| INV-1002 | Globex | Pending | $5,105.00 |
| INV-1003 | Initech | Overdue | $4,730.00 |
| INV-1004 | Umbrella | Paid | $4,355.00 |
| INV-1005 | Hooli | Pending | $3,980.00 |
Card footer
A compact simple paginator aligned to the end of a Card footer with className="justify-end".
- MCMaya Chen deployed acme-web to production2m ago
- OFOmar Farouk merged #482 Add checkout retries18m ago
- LPLena Park invited 3 people to Growth1h ago
Accessibility
- Rendered in a
<nav aria-label="pagination">landmark. Pass your ownaria-label(for example "Search results pages") when a page has more than one. - The current page has
aria-current="page". Paginatorlabels its buttons "Previous page", "Next page" and "Page 3"; the link parts label Previous and Next "Go to previous page" and "Go to next page". Override any of them witharia-label.- Previous and Next are disabled (and removed from the tab order) at the ends.
- Ellipses are
aria-hidden. - After changing pages in client-side lists, consider moving focus to the top of the results or announcing the change, so screen-reader users know the content updated.
Keyboard
| Key | Action |
|---|---|
| Tab / Shift+Tab | Moves between the controls |
| Enter | Activates the focused page, Previous or Next |
| Space | Activates a Paginator button (links respond to Enter only) |
Styling
Data attributes
On PaginationLink, PaginationPrevious, PaginationNext and the Paginator buttons:
| Attribute | Present when |
|---|---|
data-hovered / data-pressed | Hovered / being pressed |
data-focus-visible | Focused with the keyboard |
data-disabled | Disabled |
data-active | Link parts with isActive |
data-current | Link parts with isActive (from aria-current) |
Slots
data-slot | Element |
|---|---|
pagination | <nav> root |
pagination-content | <ul> |
pagination-item | Each <li> |
pagination-link | Each link part |
pagination-ellipsis | Ellipsis |
Customizing
- Items use
buttonVariants, so they follow button tones and radii. Page numbers aretabular-numsso widths don't shift. classNameonPaginationorPaginatorstyles the<nav>. It's centered withmx-auto flex w-full justify-center; usejustify-start,justify-endormx-0 w-autoto align it.classNameonPaginationLinkmust be a string.
API Reference
Paginator
Prop
Type
Also accepts every prop of <nav> except onChange.
PaginationLink
Prop
Type
Also accepts every prop of React Aria's Link except a function className.
PaginationPrevious / PaginationNext
Accept the same props as PaginationLink. Their content is fixed (chevron and label); aria-label defaults to "Go to previous page" / "Go to next page".
Pagination / PaginationContent / PaginationItem / PaginationEllipsis
Accept every prop of <nav> / <ul> / <li> / <span>.
getPageRange
Prop
Type
Returns (number | "ellipsis")[]: always the first and last page, the current page with its siblings, and "ellipsis" for each gap. When pageCount is at most siblings * 2 + 5, returns every page.