Pagination
Composable pagination controls (Pagination / PaginationContent / PaginationItem / PaginationLink / PaginationPrevious / PaginationNext / PaginationEllipsis). Link-based by default — pair with client-side navigation or server params.
Short (3 pages)
Long range with ellipsis
Installation
pnpm dlx @hex-core/cli add paginationCompact (Prev / Next only)
Drop the numbered links when the total page count would be too noisy (e.g. server-driven cursor pagination, or long catalogues where a numeric sequence offers no signal). The position label keeps users oriented.
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<span className="px-3 text-sm text-muted-foreground" aria-live="polite">
Page 4 of 27
</span>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>With page-size selector
Pair the pagination control with a Select that drives row count per page. Reset to page 1 whenever the size changes so the user doesn't land on a now-empty range.
"use client";
import { useState } from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Label,
} from "@/components/ui";
const PAGE_SIZE_OPTIONS = ["10", "25", "50"] as const;
type PageSize = (typeof PAGE_SIZE_OPTIONS)[number];
function isPageSize(value: string): value is PageSize {
return (PAGE_SIZE_OPTIONS as readonly string[]).includes(value);
}
export function Example() {
const [pageSize, setPageSize] = useState<PageSize>("25");
const [pageIndex, setPageIndex] = useState(0);
return (
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Label htmlFor="page-size" className="text-sm text-muted-foreground">
Rows per page
</Label>
<Select
value={pageSize}
onValueChange={(value) => {
if (isPageSize(value)) {
setPageSize(value);
setPageIndex(0);
}
}}
>
<SelectTrigger id="page-size" className="h-9 w-[80px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Pagination className="mx-0 w-auto justify-end">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(event) => {
event.preventDefault();
setPageIndex((i) => Math.max(0, i - 1));
}}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
{pageIndex + 1}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
onClick={(event) => {
event.preventDefault();
setPageIndex((i) => i + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the <nav> |
AI Guidance
When to use
Use for navigating between pages of a paginated dataset: blog lists, search results, table rows. Use PaginationEllipsis to truncate long ranges.
When not to use
Don't use for infinite scroll (use IntersectionObserver). Don't use for step-by-step wizards (use a stepper). Don't use for fewer than ~3 pages (just show all the items).
Common mistakes
- Using PaginationLink without href (anchor is not keyboard-reachable)
- Forgetting isActive on the current page (no visual or aria-current feedback)
- Showing every page number on long ranges — use PaginationEllipsis to truncate
- Using onClick-only <button> instead of PaginationLink — loses right-click-open-new-tab affordance; prefer href + Next.js Link when client-side routing is needed
Accessibility
Root is role='navigation' aria-label='pagination'. Active link gets aria-current='page'. Previous/Next have aria-label. Ellipsis is decorative (aria-hidden) with a sr-only 'More pages' label.
Related components
Token budget: 500
Verified against @hex-core/components@1.12.0