Pagination
Composable pagination primitives for navigating paged content without routing or data logic.
The Pagination component provides composable UI primitives for navigating paged content.
It is intentionally dumb:
- no router integration
- no fetching logic
- no URL sync
- no cursor logic
Pagination is fully controlled by the page or data layer. The UI only renders navigation.
Import
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
buildPaginationRange,
} from "@/components/ui/pagination";Usage
Pagination is composed of small building blocks:
- Pagination → wrapper (
nav) - PaginationContent → list container (
ul) - PaginationItem → item wrapper (
li) - PaginationLink → page button (active state)
- PaginationPrevious / PaginationNext → navigation
- PaginationEllipsis → compact overflow indicator
Control page state from your page or component.
Basic pagination
Page 1 / 12
function PaginationBasicExample() {
const [page, setPage] = React.useState(1);
const totalPages = 12;
const tokens = buildPaginationRange({ page, totalPages });
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={page === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
/>
</PaginationItem>
{tokens.map((t) =>
t.type === "ellipsis" ? (
<PaginationItem key={t.key}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={t.value}>
<PaginationLink
isActive={t.isActive}
onClick={() => setPage(t.value)}
>
{t.value}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
disabled={page === totalPages}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}Table + Pagination (mock data)
This example demonstrates a realistic SaaS pattern: a Table with a local Pagination control.
No backend logic is included — replace the mock slice with your real data layer later.
| Name | Status | Updated | Actions |
|---|---|---|---|
| Project 1 | Active | 2026-01-21 | |
| Project 2 | Active | 2026-01-21 | |
| Project 3 | Paused | 2026-01-21 | |
| Project 4 | Active | 2026-01-21 | |
| Project 5 | Archived | 2026-01-21 |
Showing 1–5 of 23
type Project = {
id: string;
name: string;
status: "active" | "paused" | "archived";
updatedAt: string;
};
// Example idea (pseudo):
// - keep `page` in state
// - compute a slice from your dataset
// - pass `page` + `totalPages` into Pagination
//
// const pageSize = 10;
// const totalPages = Math.ceil(projects.length / pageSize);
// const pageProjects = projects.slice((page - 1) * pageSize, page * pageSize);API
Components
| Component | Description |
|---|---|
Pagination | Wrapper nav element (aria-label="Pagination") |
PaginationContent | List container (ul) |
PaginationItem | Item wrapper (li) |
PaginationLink | Page button with active state |
PaginationPrevious | Previous button (responsive label) |
PaginationNext | Next button (responsive label) |
PaginationEllipsis | Visual overflow indicator |
PaginationLink props
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | false | Marks the current page (aria-current="page") |
size | string | "icon-sm" | Maps to Button sizes |
className | string | — | Additional Tailwind classes |
Accessibility
- Uses semantic
navwitharia-label="Pagination". - Active page sets
aria-current="page". - Buttons remain fully keyboard accessible.
- Ellipsis is non-interactive and screen-reader labeled.
Design guidelines
- Keep pagination UI-only, controlled by the page.
- Prefer small, consistent page sizes for tables.
- Use ellipsis for large page counts.
- Sync with URL and data layers in guides or patterns, not inside the component.
When not to use Pagination
Do not use Pagination for:
- infinite scroll experiences
- cursor-based APIs without a page concept
- global navigation logic
Those belong in patterns or product-level implementations.