Pagination
A robust pagination component with page navigation, jump-to-page, and items-per-page controls.
Basic Usage
The basic pagination shows previous/next buttons with page numbers.
"use client";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
PaginationEllipsis,
usePagination,
} from "@repo/ui/components/pagination";
/* BASIC PAGINATION */
export const Example1 = () => {
const pagination = usePagination({
totalItems: 100,
itemsPerPage: 10,
});
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onPress={pagination.previousPage}
isDisabled={!pagination.hasPreviousPage}
/>
</PaginationItem>
{pagination.pageRange.map((page, index) =>
page === "ellipsis" ? (
<PaginationItem key={`ellipsis-${index}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<PaginationLink
page={page}
isActive={page === pagination.currentPage}
onPress={() => pagination.goToPage(page)}
/>
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
onPress={pagination.nextPage}
isDisabled={!pagination.hasNextPage}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
);
};
With First/Last Page Buttons
Navigate directly to the first or last page with dedicated buttons. Useful for large datasets.
"use client";
import { FullPagination, usePagination } from "@repo/ui/components/pagination";
/* WITH FIRST/LAST PAGE BUTTONS */
export const Example2 = () => {
const pagination = usePagination({
totalItems: 500,
itemsPerPage: 10,
initialPage: 25,
});
return (
<FullPagination
pagination={pagination}
showFirstLast={true}
showPrevNext={true}
showPageNumbers={true}
/>
);
};
Jump to Page
Add a page input field to jump directly to any page. Perfect for when you have many pages.
"use client";
import { FullPagination, usePagination } from "@repo/ui/components/pagination";
/* WITH JUMP TO PAGE */
export const Example3 = () => {
const pagination = usePagination({
totalItems: 1000,
itemsPerPage: 10,
});
return (
<FullPagination
pagination={pagination}
showFirstLast={true}
showJump={true}
/>
);
};
Items Per Page Selector
Let users control how many items they want to see per page.
"use client";
import { FullPagination, usePagination } from "@repo/ui/components/pagination";
/* WITH ITEMS PER PAGE SELECTOR */
export const Example4 = () => {
const pagination = usePagination({
totalItems: 500,
itemsPerPage: 10,
onPageChange: (page) => console.log("Page changed to:", page),
onItemsPerPageChange: (count) => console.log("Items per page:", count),
});
return (
<FullPagination
pagination={pagination}
showPerPage={true}
perPageOptions={[5, 10, 25, 50, 100]}
/>
);
};
Page Info Display
Show the current page position or item range for better context.
Showing 1 - 10 of 247
10 items per page"use client";
import {
FullPagination,
usePagination,
PaginationInfo,
} from "@repo/ui/components/pagination";
/* WITH PAGE INFO */
export const Example5 = () => {
const pagination = usePagination({
totalItems: 247,
itemsPerPage: 10,
});
return (
<div className="flex w-full flex-col gap-4">
{/* Page X of Y format */}
<FullPagination pagination={pagination} showInfo={true} />
{/* Custom info component */}
<div className="border-border flex flex-col items-start justify-between gap-2 rounded-lg border p-4 sm:flex-row sm:items-center">
<PaginationInfo
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
totalItems={247}
startIndex={pagination.startIndex}
endIndex={pagination.endIndex}
showItemRange={true}
/>
<span className="text-muted-foreground text-sm">
{pagination.itemsPerPage} items per page
</span>
</div>
</div>
);
};
Full Featured
Combine all features for a comprehensive pagination experience.
"use client";
import { FullPagination, usePagination } from "@repo/ui/components/pagination";
/* ALL FEATURES COMBINED */
export const Example6 = () => {
const pagination = usePagination({
totalItems: 1500,
itemsPerPage: 25,
siblingCount: 1,
});
return (
<div className="w-full">
<FullPagination
pagination={pagination}
showFirstLast={true}
showPrevNext={true}
showPageNumbers={true}
showInfo={true}
showItemRange={true}
showJump={true}
showPerPage={true}
perPageOptions={[10, 25, 50, 100]}
size="default"
className="flex-wrap justify-center gap-3"
/>
</div>
);
};
Data Table Example
A real-world example showing pagination with a data table.
| ID | Name | Role | |
|---|---|---|---|
| 1 | User 1 | user1@example.com | Admin |
| 2 | User 2 | user2@example.com | Editor |
| 3 | User 3 | user3@example.com | Viewer |
| 4 | User 4 | user4@example.com | Admin |
| 5 | User 5 | user5@example.com | Editor |
"use client";
import { FullPagination, usePagination } from "@repo/ui/components/pagination";
/* DATA TABLE EXAMPLE */
const generateData = (count: number) =>
Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: ["Admin", "Editor", "Viewer"][i % 3],
}));
const allData = generateData(87);
export const Example7 = () => {
const pagination = usePagination({
totalItems: allData.length,
itemsPerPage: 5,
});
const currentData = allData.slice(
pagination.startIndex,
pagination.endIndex,
);
return (
<div className="flex w-full flex-col gap-4">
{/* Table Container - Responsive */}
<div className="w-full overflow-hidden rounded-lg">
<div className="overflow-x-auto">
<table className="w-full min-w-[500px] text-sm">
<thead>
<tr className="border-border bg-muted/50 border-b">
<th className="whitespace-nowrap px-4 py-3 text-left font-medium">
ID
</th>
<th className="whitespace-nowrap px-4 py-3 text-left font-medium">
Name
</th>
<th className="whitespace-nowrap px-4 py-3 text-left font-medium">
Email
</th>
<th className="whitespace-nowrap px-4 py-3 text-left font-medium">
Role
</th>
</tr>
</thead>
<tbody>
{currentData.map((row, index) => (
<tr
key={row.id}
className={
index < currentData.length - 1
? "border-border border-b"
: ""
}
>
<td className="whitespace-nowrap px-4 py-3 tabular-nums">
{row.id}
</td>
<td className="whitespace-nowrap px-4 py-3 font-medium">
{row.name}
</td>
<td className="text-muted-foreground whitespace-nowrap px-4 py-3">
{row.email}
</td>
<td className="whitespace-nowrap px-4 py-3">
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${
row.role === "Admin"
? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
: row.role === "Editor"
? "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400"
}`}
>
{row.role}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Pagination - Responsive */}
<FullPagination
pagination={pagination}
showInfo={true}
showItemRange={true}
showPerPage={true}
perPageOptions={[5, 10, 20, 50]}
showFirstLast={false}
className="flex-wrap justify-between gap-2"
/>
</div>
);
};
Component Code
"use client";
import { forwardRef, useRef, useCallback, useMemo, useState } from "react";
import {
AriaButtonProps,
useButton,
useFocusRing,
mergeProps,
} from "react-aria";
import { cva, type VariantProps } from "class-variance-authority";
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
import { Slot } from "@repo/ui/components/slot";
import { cn } from "tailwind-variants";
export interface PaginationState {
/** Total number of items */
totalItems: number;
/** Number of items per page */
itemsPerPage: number;
/** Current page (1-indexed) */
currentPage: number;
}
export interface UsePaginationProps {
/** Total number of items */
totalItems: number;
/** Number of items per page */
itemsPerPage?: number;
/** Initial page (1-indexed) */
initialPage?: number;
/** Maximum number of visible page buttons */
siblingCount?: number;
/** Callback when page changes */
onPageChange?: (page: number) => void;
/** Callback when items per page changes */
onItemsPerPageChange?: (itemsPerPage: number) => void;
}
export interface UsePaginationReturn {
/** Current page (1-indexed) */
currentPage: number;
/** Total number of pages */
totalPages: number;
/** Items per page */
itemsPerPage: number;
/** Whether there's a previous page */
hasPreviousPage: boolean;
/** Whether there's a next page */
hasNextPage: boolean;
/** Go to a specific page */
goToPage: (page: number) => void;
/** Go to the next page */
nextPage: () => void;
/** Go to the previous page */
previousPage: () => void;
/** Go to the first page */
firstPage: () => void;
/** Go to the last page */
lastPage: () => void;
/** Set items per page */
setItemsPerPage: (count: number) => void;
/** Array of page numbers/ellipsis to render */
pageRange: (number | "ellipsis")[];
/** Start index of current page items (0-indexed) */
startIndex: number;
/** End index of current page items (0-indexed, exclusive) */
endIndex: number;
}
export function usePagination({
totalItems,
itemsPerPage: initialItemsPerPage = 10,
initialPage = 1,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
siblingCount = 1,
onPageChange,
onItemsPerPageChange,
}: UsePaginationProps): UsePaginationReturn {
const [currentPage, setCurrentPage] = useState(initialPage);
const [itemsPerPage, setItemsPerPageState] = useState(initialItemsPerPage);
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
const validCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
if (validCurrentPage !== currentPage) {
setCurrentPage(validCurrentPage);
}
const hasPreviousPage = validCurrentPage > 1;
const hasNextPage = validCurrentPage < totalPages;
const goToPage = useCallback(
(page: number) => {
const newPage = Math.min(Math.max(1, page), totalPages);
if (newPage !== currentPage) {
setCurrentPage(newPage);
onPageChange?.(newPage);
}
},
[currentPage, totalPages, onPageChange],
);
const nextPage = useCallback(() => {
if (hasNextPage) goToPage(currentPage + 1);
}, [hasNextPage, currentPage, goToPage]);
const previousPage = useCallback(() => {
if (hasPreviousPage) goToPage(currentPage - 1);
}, [hasPreviousPage, currentPage, goToPage]);
const firstPage = useCallback(() => goToPage(1), [goToPage]);
const lastPage = useCallback(
() => goToPage(totalPages),
[goToPage, totalPages],
);
const setItemsPerPage = useCallback(
(count: number) => {
setItemsPerPageState(count);
setCurrentPage(1);
onItemsPerPageChange?.(count);
onPageChange?.(1);
},
[onItemsPerPageChange, onPageChange],
);
const pageRange = useMemo((): (number | "ellipsis")[] => {
const minPagesToShowEllipsis = 7;
if (totalPages <= minPagesToShowEllipsis) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const first = 1;
const last = totalPages;
const center = validCurrentPage;
const minCenter = 4;
const maxCenter = totalPages - 3;
if (center < minCenter) {
return [1, 2, 3, 4, 5, "ellipsis", totalPages];
}
if (center > maxCenter) {
return [
1,
"ellipsis",
totalPages - 4,
totalPages - 3,
totalPages - 2,
totalPages - 1,
totalPages,
];
}
return [
first,
"ellipsis",
center - 1,
center,
center + 1,
"ellipsis",
last,
];
}, [totalPages, validCurrentPage]);
const startIndex = (validCurrentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
return {
currentPage: validCurrentPage,
totalPages,
itemsPerPage,
hasPreviousPage,
hasNextPage,
goToPage,
nextPage,
previousPage,
firstPage,
lastPage,
setItemsPerPage,
pageRange,
startIndex,
endIndex,
};
}
const paginationButtonVariants = cva(
cn(
"inline-flex items-center justify-center rounded-lg text-sm font-medium",
"focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50",
"cursor-pointer select-none",
),
{
variants: {
variant: {
default:
"bg-transparent hover:bg-accent hover:text-accent-foreground",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
active: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
ghost: "bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 min-w-9 px-3",
sm: "h-8 min-w-8 px-2 text-xs",
lg: "h-10 min-w-10 px-4",
icon: "h-9 w-9",
"icon-sm": "h-8 w-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
interface PaginationProps extends React.ComponentProps<"nav"> {
"aria-label"?: string;
}
export const Pagination = forwardRef<HTMLElement, PaginationProps>(
({ className, "aria-label": ariaLabel = "Pagination", ...props }, ref) => {
return (
<nav
ref={ref}
role="navigation"
aria-label={ariaLabel}
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
},
);
Pagination.displayName = "Pagination";
export const PaginationContent = forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => {
return (
<ul
ref={ref}
data-slot="pagination-content"
className={cn(
"flex shrink-0 list-none flex-row flex-wrap items-center gap-1",
className,
)}
{...props}
/>
);
});
PaginationContent.displayName = "PaginationContent";
export const PaginationItem = forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => {
return (
<li
ref={ref}
data-slot="pagination-item"
className={cn("list-none", className)}
{...props}
/>
);
});
PaginationItem.displayName = "PaginationItem";
interface PaginationButtonProps
extends Omit<AriaButtonProps<"button">, "elementType">,
VariantProps<typeof paginationButtonVariants> {
className?: string;
children?: React.ReactNode;
asChild?: boolean;
}
export const PaginationButton = forwardRef<
HTMLButtonElement,
PaginationButtonProps
>(
(
{ variant, size, className, children, asChild = false, ...props },
forwardedRef,
) => {
const internalRef = useRef<HTMLButtonElement | null>(null);
const mergedRef = (node: HTMLButtonElement | null) => {
internalRef.current = node;
if (typeof forwardedRef === "function") {
forwardedRef(node);
} else if (forwardedRef) {
forwardedRef.current = node;
}
};
const { buttonProps, isPressed } = useButton(props, internalRef);
const { focusProps, isFocusVisible } = useFocusRing();
const Comp = asChild ? Slot : "button";
return (
<Comp
{...mergeProps(buttonProps, focusProps)}
ref={mergedRef}
className={cn(
paginationButtonVariants({ variant, size }),
isFocusVisible && "ring-ring ring-2 ring-offset-2",
className,
)}
data-pressed={isPressed ? "true" : undefined}
>
{children}
</Comp>
);
},
);
PaginationButton.displayName = "PaginationButton";
interface PaginationLinkProps extends Omit<PaginationButtonProps, "variant"> {
isActive?: boolean;
page: number;
}
export const PaginationLink = forwardRef<
HTMLButtonElement,
PaginationLinkProps
>(({ isActive, page, className, children, ...props }, ref) => {
return (
<PaginationButton
ref={ref}
variant={isActive ? "active" : "ghost"}
aria-current={isActive ? "page" : undefined}
aria-label={`Page ${page}`}
data-active={isActive}
className={className}
{...props}
>
{children ?? page}
</PaginationButton>
);
});
PaginationLink.displayName = "PaginationLink";
interface PaginationPreviousProps extends PaginationButtonProps {
showLabel?: boolean;
label?: string;
}
export const PaginationPrevious = forwardRef<
HTMLButtonElement,
PaginationPreviousProps
>(
(
{ showLabel = true, label = "Previous", className, children, ...props },
ref,
) => {
return (
<PaginationButton
ref={ref}
aria-label="Go to previous page"
variant="ghost"
size={showLabel ? "default" : "icon"}
className={cn("gap-1", showLabel && "px-2.5", className)}
{...props}
>
{children ?? (
<>
<ChevronLeftIcon className="h-4 w-4" />
{showLabel && (
<span className="hidden sm:inline">{label}</span>
)}
</>
)}
</PaginationButton>
);
},
);
PaginationPrevious.displayName = "PaginationPrevious";
interface PaginationNextProps extends PaginationButtonProps {
showLabel?: boolean;
label?: string;
}
export const PaginationNext = forwardRef<
HTMLButtonElement,
PaginationNextProps
>(
(
{ showLabel = true, label = "Next", className, children, ...props },
ref,
) => {
return (
<PaginationButton
ref={ref}
aria-label="Go to next page"
variant="ghost"
size={showLabel ? "default" : "icon"}
className={cn("gap-1", showLabel && "px-2.5", className)}
{...props}
>
{children ?? (
<>
{showLabel && (
<span className="hidden sm:inline">{label}</span>
)}
<ChevronRightIcon className="h-4 w-4" />
</>
)}
</PaginationButton>
);
},
);
PaginationNext.displayName = "PaginationNext";
interface PaginationFirstProps extends PaginationButtonProps {
showLabel?: boolean;
label?: string;
}
export const PaginationFirst = forwardRef<
HTMLButtonElement,
PaginationFirstProps
>(
(
{ showLabel = false, label = "First", className, children, ...props },
ref,
) => {
return (
<PaginationButton
ref={ref}
aria-label="Go to first page"
variant="ghost"
size={showLabel ? "default" : "icon"}
className={cn("gap-1", className)}
{...props}
>
{children ?? (
<>
<ChevronsLeftIcon className="h-4 w-4" />
{showLabel && (
<span className="hidden sm:inline">{label}</span>
)}
</>
)}
</PaginationButton>
);
},
);
PaginationFirst.displayName = "PaginationFirst";
interface PaginationLastProps extends PaginationButtonProps {
showLabel?: boolean;
label?: string;
}
export const PaginationLast = forwardRef<
HTMLButtonElement,
PaginationLastProps
>(
(
{ showLabel = false, label = "Last", className, children, ...props },
ref,
) => {
return (
<PaginationButton
ref={ref}
aria-label="Go to last page"
variant="ghost"
size={showLabel ? "default" : "icon"}
className={cn("gap-1", className)}
{...props}
>
{children ?? (
<>
{showLabel && (
<span className="hidden sm:inline">{label}</span>
)}
<ChevronsRightIcon className="h-4 w-4" />
</>
)}
</PaginationButton>
);
},
);
PaginationLast.displayName = "PaginationLast";
interface PaginationEllipsisProps extends React.ComponentProps<"span"> {
onJump?: () => void;
}
export const PaginationEllipsis = forwardRef<
HTMLSpanElement,
PaginationEllipsisProps
>(({ className, onJump, ...props }, ref) => {
const isClickable = !!onJump;
if (isClickable) {
return (
<button
type="button"
onClick={onJump}
className={cn(
"flex h-9 w-9 items-center justify-center rounded-lg",
"hover:bg-accent hover:text-accent-foreground",
"cursor-pointer",
className,
)}
aria-label="Jump to page"
>
<MoreHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</button>
);
}
return (
<span
ref={ref}
aria-hidden
data-slot="pagination-ellipsis"
className={cn(
"flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
});
PaginationEllipsis.displayName = "PaginationEllipsis";
interface PaginationInfoProps extends React.ComponentProps<"div"> {
currentPage: number;
totalPages: number;
totalItems?: number;
startIndex?: number;
endIndex?: number;
showItemRange?: boolean;
}
export const PaginationInfo = forwardRef<HTMLDivElement, PaginationInfoProps>(
(
{
currentPage,
totalPages,
totalItems,
startIndex,
endIndex,
showItemRange = false,
className,
...props
},
ref,
) => {
return (
<div
ref={ref}
data-slot="pagination-info"
className={cn(
"text-muted-foreground min-w-[180px] text-sm tabular-nums",
className,
)}
{...props}
>
{showItemRange &&
totalItems !== undefined &&
startIndex !== undefined &&
endIndex !== undefined ? (
<span>
Showing{" "}
<span className="text-foreground font-medium">
{startIndex + 1}
</span>
{" - "}
<span className="text-foreground font-medium">
{endIndex}
</span>
{" of "}
<span className="text-foreground font-medium">
{totalItems}
</span>
</span>
) : (
<span>
Page{" "}
<span className="text-foreground font-medium">
{currentPage}
</span>
{" of "}
<span className="text-foreground font-medium">
{totalPages}
</span>
</span>
)}
</div>
);
},
);
PaginationInfo.displayName = "PaginationInfo";
interface PaginationJumpProps
extends Omit<React.ComponentProps<"div">, "onChange"> {
totalPages: number;
onJump: (page: number) => void;
label?: string;
placeholder?: string;
}
export const PaginationJump = forwardRef<HTMLDivElement, PaginationJumpProps>(
(
{
totalPages,
onJump,
label = "Go to",
placeholder,
className,
...props
},
ref,
) => {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState("");
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const page = parseInt(inputValue, 10);
if (!isNaN(page) && page >= 1 && page <= totalPages) {
onJump(page);
setInputValue("");
inputRef.current?.blur();
}
},
[inputValue, totalPages, onJump],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSubmit(e);
} else if (e.key === "Escape") {
setInputValue("");
inputRef.current?.blur();
}
},
[handleSubmit],
);
return (
<div
ref={ref}
data-slot="pagination-jump"
className={cn("flex items-center gap-2 text-sm", className)}
{...props}
>
<label
htmlFor="pagination-jump-input"
className="text-muted-foreground whitespace-nowrap"
>
{label}
</label>
<form
onSubmit={handleSubmit}
className="flex items-center gap-1"
>
<input
ref={inputRef}
id="pagination-jump-input"
type="number"
min={1}
max={totalPages}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder ?? `1-${totalPages}`}
className={cn(
"border-input bg-background h-8 w-16 rounded-md border px-2 text-center text-sm",
"placeholder:text-muted-foreground",
"focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
)}
aria-label={`Jump to page, enter a number between 1 and ${totalPages}`}
/>
<PaginationButton
type="submit"
size="sm"
variant="outline"
className="hidden sm:inline-flex"
>
Go
</PaginationButton>
</form>
</div>
);
},
);
PaginationJump.displayName = "PaginationJump";
interface PaginationPerPageProps
extends Omit<React.ComponentProps<"div">, "onChange"> {
value: number;
options?: number[];
onChange: (value: number) => void;
label?: string;
}
export const PaginationPerPage = forwardRef<
HTMLDivElement,
PaginationPerPageProps
>(
(
{
value,
options = [10, 20, 50, 100],
onChange,
label = "Items per page",
className,
...props
},
ref,
) => {
return (
<div
ref={ref}
data-slot="pagination-per-page"
className={cn("flex items-center gap-2 text-sm", className)}
{...props}
>
<label
htmlFor="pagination-per-page-select"
className="text-muted-foreground whitespace-nowrap"
>
{label}
</label>
<select
id="pagination-per-page-select"
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className={cn(
"border-input bg-background h-8 rounded-md border px-2 text-sm",
"focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"cursor-pointer",
)}
aria-label="Select number of items per page"
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
);
},
);
PaginationPerPage.displayName = "PaginationPerPage";
export interface FullPaginationProps
extends Omit<React.ComponentProps<"nav">, "onChange"> {
/** Pagination state from usePagination hook, or controlled props */
pagination: UsePaginationReturn;
/** Show first/last page buttons */
showFirstLast?: boolean;
/** Show previous/next buttons */
showPrevNext?: boolean;
/** Show page numbers */
showPageNumbers?: boolean;
/** Show page info (e.g., "Page 1 of 10") */
showInfo?: boolean;
/** Show item range instead of page info */
showItemRange?: boolean;
/** Show jump to page input */
showJump?: boolean;
/** Show items per page selector */
showPerPage?: boolean;
/** Items per page options */
perPageOptions?: number[];
/** Size variant */
size?: "default" | "sm" | "lg";
/** Labels for i18n */
labels?: {
previous?: string;
next?: string;
first?: string;
last?: string;
goTo?: string;
perPage?: string;
};
}
export const FullPagination = forwardRef<HTMLElement, FullPaginationProps>(
(
{
pagination,
showFirstLast = true,
showPrevNext = true,
showPageNumbers = true,
showInfo = false,
showItemRange = false,
showJump = false,
showPerPage = false,
perPageOptions,
size = "default",
labels = {},
className,
...props
},
ref,
) => {
const {
currentPage,
totalPages,
itemsPerPage,
hasPreviousPage,
hasNextPage,
goToPage,
nextPage,
previousPage,
firstPage,
lastPage,
setItemsPerPage,
pageRange,
startIndex,
endIndex,
} = pagination;
const sizeMap = {
default: { button: "default" as const, icon: "icon" as const },
sm: { button: "sm" as const, icon: "icon-sm" as const },
lg: { button: "lg" as const, icon: "icon" as const },
};
const currentSize = sizeMap[size];
const totalItems =
pagination.startIndex !== undefined &&
pagination.endIndex !== undefined
? Math.ceil(pagination.endIndex / pagination.currentPage) *
pagination.totalPages
: undefined;
return (
<Pagination
ref={ref}
className={cn("flex-col gap-4 sm:flex-row", className)}
{...props}
>
{(showInfo || showItemRange) && totalItems && (
<PaginationInfo
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
startIndex={startIndex}
endIndex={endIndex}
showItemRange={showItemRange}
className="sm:order-0 order-first"
/>
)}
<PaginationContent>
{showFirstLast && (
<PaginationItem>
<PaginationFirst
onPress={firstPage}
isDisabled={!hasPreviousPage}
size={currentSize.icon}
label={labels.first}
/>
</PaginationItem>
)}
{showPrevNext && (
<PaginationItem>
<PaginationPrevious
onPress={previousPage}
isDisabled={!hasPreviousPage}
size={currentSize.button}
label={labels.previous}
/>
</PaginationItem>
)}
{showPageNumbers &&
pageRange.map((page, index) => {
const ellipsisKey =
page === "ellipsis"
? index < pageRange.indexOf(currentPage) ||
(pageRange.indexOf(currentPage) === -1 &&
index < pageRange.length / 2)
? "ellipsis-left"
: "ellipsis-right"
: page;
return page === "ellipsis" ? (
<PaginationItem key={ellipsisKey}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<PaginationLink
page={page}
isActive={page === currentPage}
onPress={() => goToPage(page)}
size={currentSize.icon}
/>
</PaginationItem>
);
})}
{showPrevNext && (
<PaginationItem>
<PaginationNext
onPress={nextPage}
isDisabled={!hasNextPage}
size={currentSize.button}
label={labels.next}
/>
</PaginationItem>
)}
{showFirstLast && (
<PaginationItem>
<PaginationLast
onPress={lastPage}
isDisabled={!hasNextPage}
size={currentSize.icon}
label={labels.last}
/>
</PaginationItem>
)}
</PaginationContent>
{showJump && (
<PaginationJump
totalPages={totalPages}
onJump={goToPage}
label={labels.goTo}
className="sm:order-0 order-last"
/>
)}
{showPerPage && (
<PaginationPerPage
value={itemsPerPage}
options={perPageOptions}
onChange={setItemsPerPage}
label={labels.perPage}
className="order-last"
/>
)}
</Pagination>
);
},
);
FullPagination.displayName = "FullPagination";
export { paginationButtonVariants };