Pagination
Displays page numbers and controls for navigating paginated data.
The Pagination component provides navigation controls for paginated content. It supports previous/next buttons, page numbers, ellipsis for skipped pages, and customizable styling. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/pagination.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
currentPage?: number
totalPages?: number
onPageChange?: (page: number) => void
showFirstLast?: boolean
siblingCount?: number
}
const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
({ className, currentPage = 1, totalPages = 10, onPageChange, showFirstLast = false, siblingCount = 1, ...props }, ref) => {
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages && page !== currentPage) {
onPageChange?.(page)
}
}
const getPageNumbers = () => {
const pages: (number | string)[] = []
const totalNumbers = siblingCount * 2 + 5
const totalBlocks = totalNumbers + 2
if (totalPages <= totalBlocks) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
return pages
}
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1)
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages)
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < totalPages - 1
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = 3 + 2 * siblingCount
const leftRange: number[] = []
for (let i = 1; i <= leftItemCount; i++) {
leftRange.push(i)
}
return [...leftRange, "...", totalPages]
}
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblingCount
const rightRange: number[] = []
for (let i = totalPages - rightItemCount + 1; i <= totalPages; i++) {
rightRange.push(i)
}
return [1, "...", ...rightRange]
}
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange: number[] = []
for (let i = leftSiblingIndex; i <= rightSiblingIndex; i++) {
middleRange.push(i)
}
return [1, "...", ...middleRange, "...", totalPages]
}
return pages
}
const pageNumbers = getPageNumbers()
return (
<nav
ref={ref}
className={cn("flex items-center gap-1", className)}
aria-label="Pagination"
{...props}
>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
/>
{showFirstLast && currentPage > siblingCount + 2 && (
<>
<PaginationItem onClick={() => handlePageChange(1)}>
{1}
</PaginationItem>
{currentPage > siblingCount + 3 && (
<PaginationEllipsis />
)}
</>
)}
{pageNumbers.map((page, index) => {
if (page === "...") {
return <PaginationEllipsis key={`ellipsis-${index}`} />
}
return (
<PaginationItem
key={page}
onClick={() => handlePageChange(page as number)}
isActive={currentPage === page}
>
{page}
</PaginationItem>
)
})}
{showFirstLast && currentPage < totalPages - siblingCount - 1 && (
<>
{currentPage < totalPages - siblingCount - 2 && (
<PaginationEllipsis />
)}
<PaginationItem onClick={() => handlePageChange(totalPages)}>
{totalPages}
</PaginationItem>
</>
)}
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
/>
</nav>
)
}
)
Pagination.displayName = "Pagination"
interface PaginationLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean
}
const PaginationItem = React.forwardRef<HTMLButtonElement, PaginationLinkProps>(
({ className, isActive, children, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 w-10 items-center justify-center rounded-md border-2 border-foreground text-sm font-bold transition-colors",
isActive
? "bg-primary text-primary-foreground neobrutalism-shadow-sm"
: "bg-background hover:bg-muted",
className
)}
{...props}
>
{children}
</button>
)
)
PaginationItem.displayName = "PaginationItem"
interface PaginationPreviousProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const PaginationPrevious = React.forwardRef<HTMLButtonElement, PaginationPreviousProps>(
({ className, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 items-center justify-center gap-1 rounded-md border-2 border-foreground bg-background px-4 text-sm font-bold transition-colors hover:bg-muted disabled:opacity-50 disabled:pointer-events-none",
className
)}
{...props}
>
<span>‹</span>
<span>Previous</span>
</button>
)
)
PaginationPrevious.displayName = "PaginationPrevious"
interface PaginationNextProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const PaginationNext = React.forwardRef<HTMLButtonElement, PaginationNextProps>(
({ className, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 items-center justify-center gap-1 rounded-md border-2 border-foreground bg-background px-4 text-sm font-bold transition-colors hover:bg-muted disabled:opacity-50 disabled:pointer-events-none",
className
)}
{...props}
>
<span>Next</span>
<span>›</span>
</button>
)
)
PaginationNext.displayName = "PaginationNext"
interface PaginationEllipsisProps extends React.HTMLAttributes<HTMLSpanElement> {}
const PaginationEllipsis = React.forwardRef<HTMLSpanElement, PaginationEllipsisProps>(
({ className, ...props }, ref) => (
<span
ref={ref}
className={cn("flex h-10 w-10 items-center justify-center text-sm font-bold", className)}
{...props}
>
...
</span>
)
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}JavaScript: Copy this code into components/ui/pagination.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const Pagination = React.forwardRef(
({ className, currentPage = 1, totalPages = 10, onPageChange, showFirstLast = false, siblingCount = 1, ...props }, ref) => {
const handlePageChange = (page) => {
if (page >= 1 && page <= totalPages && page !== currentPage) {
onPageChange?.(page)
}
}
const getPageNumbers = () => {
const pages = []
const totalNumbers = siblingCount * 2 + 5
const totalBlocks = totalNumbers + 2
if (totalPages <= totalBlocks) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
return pages
}
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1)
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages)
const shouldShowLeftDots = leftSiblingIndex > 2
const shouldShowRightDots = rightSiblingIndex < totalPages - 1
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = 3 + 2 * siblingCount
const leftRange = []
for (let i = 1; i <= leftItemCount; i++) {
leftRange.push(i)
}
return [...leftRange, "...", totalPages]
}
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = 3 + 2 * siblingCount
const rightRange = []
for (let i = totalPages - rightItemCount + 1; i <= totalPages; i++) {
rightRange.push(i)
}
return [1, "...", ...rightRange]
}
if (shouldShowLeftDots && shouldShowRightDots) {
const middleRange = []
for (let i = leftSiblingIndex; i <= rightSiblingIndex; i++) {
middleRange.push(i)
}
return [1, "...", ...middleRange, "...", totalPages]
}
return pages
}
const pageNumbers = getPageNumbers()
return (
<nav
ref={ref}
className={cn("flex items-center gap-1", className)}
aria-label="Pagination"
{...props}
>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
/>
{showFirstLast && currentPage > siblingCount + 2 && (
<>
<PaginationItem onClick={() => handlePageChange(1)}>
{1}
</PaginationItem>
{currentPage > siblingCount + 3 && (
<PaginationEllipsis />
)}
</>
)}
{pageNumbers.map((page, index) => {
if (page === "...") {
return <PaginationEllipsis key={`ellipsis-${index}`} />
}
return (
<PaginationItem
key={page}
onClick={() => handlePageChange(page)}
isActive={currentPage === page}
>
{page}
</PaginationItem>
)
})}
{showFirstLast && currentPage < totalPages - siblingCount - 1 && (
<>
{currentPage < totalPages - siblingCount - 2 && (
<PaginationEllipsis />
)}
<PaginationItem onClick={() => handlePageChange(totalPages)}>
{totalPages}
</PaginationItem>
</>
)}
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
/>
</nav>
)
}
)
Pagination.displayName = "Pagination"
const PaginationItem = React.forwardRef(
({ className, isActive, children, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 w-10 items-center justify-center rounded-md border-2 border-foreground text-sm font-bold transition-colors",
isActive
? "bg-primary text-primary-foreground neobrutalism-shadow-sm"
: "bg-background hover:bg-muted",
className
)}
{...props}
>
{children}
</button>
)
)
PaginationItem.displayName = "PaginationItem"
const PaginationPrevious = React.forwardRef(
({ className, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 items-center justify-center gap-1 rounded-md border-2 border-foreground bg-background px-4 text-sm font-bold transition-colors hover:bg-muted disabled:opacity-50 disabled:pointer-events-none",
className
)}
{...props}
>
<span>‹</span>
<span>Previous</span>
</button>
)
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = React.forwardRef(
({ className, ...props }, ref) => (
<button
ref={ref}
className={cn(
"flex h-10 items-center justify-center gap-1 rounded-md border-2 border-foreground bg-background px-4 text-sm font-bold transition-colors hover:bg-muted disabled:opacity-50 disabled:pointer-events-none",
className
)}
{...props}
>
<span>Next</span>
<span>›</span>
</button>
)
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = React.forwardRef(
({ className, ...props }, ref) => (
<span
ref={ref}
className={cn("flex h-10 w-10 items-center justify-center text-sm font-bold", className)}
{...props}
>
...
</span>
)
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}Usage
TypeScript:
tsx
import { Pagination } from "@/components/ui/pagination"
import * as React from "react"
function MyComponent() {
const [currentPage, setCurrentPage] = React.useState(1)
return (
<Pagination
currentPage={currentPage}
totalPages={10}
onPageChange={setCurrentPage}
/>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.