Pagination
NavigationBounded pagination with compact page windows and result announcements.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { Pagination } from "@/components/ui/pagination";
import { useState } from "react";
export default function PaginationExample() {
const [page, setPage] = useState(1);
return (
<div className="w-full max-w-lg space-y-5">
<div className="rounded-xl border p-5">
<h3 className="text-sm font-medium">Customer directory</h3>
<p role="status" className="mt-2 text-sm text-muted-foreground">
Showing customers {(page - 1) * 10 + 1}–{page * 10} of 120
</p>
</div>
<Pagination page={page} totalPages={12} onPageChange={setPage} />
<div className="border-t pt-4">
<p className="mb-3 text-xs text-muted-foreground">
Disabled while loading
</p>
<Pagination
label="Loading pagination"
page={1}
totalPages={3}
onPageChange={setPage}
disabled
/>
</div>
</div>
);
}Usage
Pass a one-based page, totalPages, and onPageChange. The component displays the first, last, and nearby pages with ellipses. Load or filter data in your application; Pagination does not fetch records.
import { Pagination } from "@/components/ui/pagination";Shipped as editable source in src/components/ui/pagination.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/pagination.tsx
"use client";
import { ChevronLeft, ChevronRight, Ellipsis } from "lucide-react";
import { Button } from "@dashboardpack/core/components/ui/button";
import { cn } from "@dashboardpack/core/lib/utils";
export interface PaginationProps {
page: number;
totalPages: number;
onPageChange: (page: number) => void;
disabled?: boolean;
className?: string;
label?: string;
}
export function Pagination({
page,
totalPages,
onPageChange,
disabled,
className,
label = "Pagination",
}: PaginationProps) {
const total = Number.isFinite(totalPages)
? Math.max(1, Math.floor(totalPages))
: 1;
const current = Number.isFinite(page)
? Math.min(total, Math.max(1, Math.floor(page)))
: 1;
const pages = Array.from(
new Set(
[1, current - 1, current, current + 1, total].filter(
(n) => n >= 1 && n <= total,
),
),
).sort((a, b) => a - b);
return (
<nav
aria-label={label}
className={cn("flex flex-wrap items-center gap-1", className)}
>
<Button
type="button"
size="icon-sm"
variant="outline"
aria-label="Previous page"
disabled={disabled || current === 1}
onClick={() => onPageChange(current - 1)}
>
<ChevronLeft className="size-4 rtl:rotate-180" />
</Button>
{pages.map((n, index) => (
<span key={n} className="inline-flex items-center gap-1">
{index > 0 && n - pages[index - 1] > 1 && (
<span aria-hidden className="px-1">
<Ellipsis className="size-4" />
</span>
)}
<Button
type="button"
size="icon-sm"
variant={n === current ? "default" : "ghost"}
aria-label={`Page ${n}`}
aria-current={n === current ? "page" : undefined}
disabled={disabled}
onClick={() => onPageChange(n)}
>
{n}
</Button>
</span>
))}
<Button
type="button"
size="icon-sm"
variant="outline"
aria-label="Next page"
disabled={disabled || current === total}
onClick={() => onPageChange(current + 1)}
>
<ChevronRight className="size-4 rtl:rotate-180" />
</Button>
<span role="status" className="ms-2 text-xs text-muted-foreground">
Page {current} of {total}
</span>
</nav>
);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| page | numberDefault: required | One-based current page, visually clamped to valid bounds. |
| totalPages | numberDefault: required | Page count; zero or invalid values display one page. |
| onPageChange | (page: number) => voidDefault: required | Receives the requested page. |
| disabled | booleanDefault: false | Disable navigation while loading. |
| label | stringDefault: Pagination | Accessible navigation landmark name. |