Components
Advanced Pagination
Advanced Pagination
Sophisticated page navigation with animated transitions, page size controls, and accessibility features.
5
of 20 pages
Showing 41-50 of 200 items
Jump to:
Installation
1
Install the packages
npm i motion clsx tailwind-merge react-icons2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
advanced-pagination.tsx
"use client";
import { motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { FiChevronLeft, FiChevronRight, FiMoreHorizontal } from "react-icons/fi";
type AdvancedPaginationProps = {
currentPage?: number;
totalPages?: number;
onPageChange?: (page: number) => void;
showPageSize?: boolean;
};
const AdvancedPagination = ({
currentPage: initialPage = 1,
totalPages = 10,
onPageChange = () => {},
showPageSize = true,
}: AdvancedPaginationProps) => {
const [currentPage, setCurrentPage] = useState(initialPage);
const [pageSize, setPageSize] = useState(10);
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
onPageChange(page);
}
};
const getVisiblePages = () => {
const delta = 2;
const range = [];
const rangeWithDots = [];
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
range.push(i);
}
if (currentPage - delta > 2) {
rangeWithDots.push(1, '...');
} else {
rangeWithDots.push(1);
}
rangeWithDots.push(...range);
if (currentPage + delta < totalPages - 1) {
rangeWithDots.push('...', totalPages);
} else if (totalPages > 1) {
rangeWithDots.push(totalPages);
}
return rangeWithDots;
};
const visiblePages = getVisiblePages();
return (
<div className="w-full max-w-2xl mx-auto">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Page size selector */}
{showPageSize && (
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-600 dark:text-neutral-400">Show:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="px-3 py-1 text-sm border border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
<span className="text-sm text-neutral-600 dark:text-neutral-400">per page</span>
</div>
)}
{/* Pagination controls */}
<div className="flex items-center gap-1">
{/* Previous button */}
<motion.button
className={cn(
"p-2 rounded-md border transition-all duration-200",
currentPage === 1
? "border-neutral-200 dark:border-neutral-700 text-neutral-400 cursor-not-allowed"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
whileTap={{ scale: 0.95 }}
>
<FiChevronLeft className="w-4 h-4" />
</motion.button>
{/* Page numbers */}
{visiblePages.map((page, index) => (
<motion.button
key={index}
className={cn(
"px-3 py-2 mx-0.5 rounded-md border transition-all duration-200 text-sm font-medium",
page === currentPage
? "bg-blue-500 border-blue-500 text-white"
: page === '...'
? "border-transparent cursor-default"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => typeof page === 'number' && handlePageChange(page)}
disabled={page === '...'}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
{page === '...' ? <FiMoreHorizontal className="w-4 h-4" /> : page}
</motion.button>
))}
{/* Next button */}
<motion.button
className={cn(
"p-2 rounded-md border transition-all duration-200",
currentPage === totalPages
? "border-neutral-200 dark:border-neutral-700 text-neutral-400 cursor-not-allowed"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
whileTap={{ scale: 0.95 }}
>
<FiChevronRight className="w-4 h-4" />
</motion.button>
</div>
{/* Page info */}
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Page {currentPage} of {totalPages}
</div>
</div>
</div>
);
};
export default AdvancedPagination;"use client";
import { motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { FiChevronLeft, FiChevronRight, FiMoreHorizontal } from "react-icons/fi";
type AdvancedPaginationProps = {
currentPage?: number;
totalPages?: number;
onPageChange?: (page: number) => void;
showPageSize?: boolean;
};
const AdvancedPagination = ({
currentPage: initialPage = 1,
totalPages = 10,
onPageChange = () => {},
showPageSize = true,
}: AdvancedPaginationProps) => {
const [currentPage, setCurrentPage] = useState(initialPage);
const [pageSize, setPageSize] = useState(10);
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
onPageChange(page);
}
};
const getVisiblePages = () => {
const delta = 2;
const range = [];
const rangeWithDots = [];
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
range.push(i);
}
if (currentPage - delta > 2) {
rangeWithDots.push(1, '...');
} else {
rangeWithDots.push(1);
}
rangeWithDots.push(...range);
if (currentPage + delta < totalPages - 1) {
rangeWithDots.push('...', totalPages);
} else if (totalPages > 1) {
rangeWithDots.push(totalPages);
}
return rangeWithDots;
};
const visiblePages = getVisiblePages();
return (
<div className="w-full max-w-2xl mx-auto">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Page size selector */}
{showPageSize && (
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-600 dark:text-neutral-400">Show:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="px-3 py-1 text-sm border border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
<span className="text-sm text-neutral-600 dark:text-neutral-400">per page</span>
</div>
)}
{/* Pagination controls */}
<div className="flex items-center gap-1">
{/* Previous button */}
<motion.button
className={cn(
"p-2 rounded-md border transition-all duration-200",
currentPage === 1
? "border-neutral-200 dark:border-neutral-700 text-neutral-400 cursor-not-allowed"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
whileTap={{ scale: 0.95 }}
>
<FiChevronLeft className="w-4 h-4" />
</motion.button>
{/* Page numbers */}
{visiblePages.map((page, index) => (
<motion.button
key={index}
className={cn(
"px-3 py-2 mx-0.5 rounded-md border transition-all duration-200 text-sm font-medium",
page === currentPage
? "bg-blue-500 border-blue-500 text-white"
: page === '...'
? "border-transparent cursor-default"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => typeof page === 'number' && handlePageChange(page)}
disabled={page === '...'}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
{page === '...' ? <FiMoreHorizontal className="w-4 h-4" /> : page}
</motion.button>
))}
{/* Next button */}
<motion.button
className={cn(
"p-2 rounded-md border transition-all duration-200",
currentPage === totalPages
? "border-neutral-200 dark:border-neutral-700 text-neutral-400 cursor-not-allowed"
: "border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800"
)}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
whileTap={{ scale: 0.95 }}
>
<FiChevronRight className="w-4 h-4" />
</motion.button>
</div>
{/* Page info */}
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Page {currentPage} of {totalPages}
</div>
</div>
</div>
);
};
export default AdvancedPagination;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| currentPage | number | 1 | The current active page number. |
| totalPages | number | 10 | The total number of pages. |
| onPageChange | function | () => {} | Callback function when page changes. |
| showPageSize | boolean | true | Whether to show the page size selector. |