Pagination
Navigate through multiple pages of content with a clean, accessible pagination component.
Usage
A fully functional pagination component with props for total pages, initial page, and page change callbacks.
'use client'import { useState, useMemo } from 'react'import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'const DOTS = '...'const range = (start, end) => {let length = end - start + 1return Array.from({ length }, (_, idx) => idx + start)}export default function Pagination({totalPages = 20,initialPage = 1,siblingCount = 1,onPageChange,...props}) {const [currentPage, setCurrentPage] = useState(initialPage)const handlePageChange = (page) => {setCurrentPage(page)onPageChange?.(page)}const paginationRange = useMemo(() => {const totalPageNumbers = siblingCount + 5if (totalPageNumbers >= totalPages) {return range(1, totalPages)}const leftSiblingIndex = Math.max(currentPage - siblingCount, 1)const rightSiblingIndex = Math.min(currentPage + siblingCount,totalPages,)const shouldShowLeftDots = leftSiblingIndex > 2const shouldShowRightDots = rightSiblingIndex < totalPages - 2const firstPageIndex = 1const lastPageIndex = totalPagesif (!shouldShowLeftDots && shouldShowRightDots) {let leftItemCount = 3 + 2 * siblingCountlet leftRange = range(1, leftItemCount)return [...leftRange, DOTS, totalPages]}if (shouldShowLeftDots && !shouldShowRightDots) {let rightItemCount = 3 + 2 * siblingCountlet rightRange = range(totalPages - rightItemCount + 1,totalPages,)return [firstPageIndex, DOTS, ...rightRange]}if (shouldShowLeftDots && shouldShowRightDots) {let middleRange = range(leftSiblingIndex, rightSiblingIndex)return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]}}, [totalPages, siblingCount, currentPage])if (currentPage === 0 || paginationRange.length < 2) {return null}const onNext = () => {const nextPage = currentPage + 1handlePageChange(nextPage)}const onPrevious = () => {const prevPage = currentPage - 1handlePageChange(prevPage)}let lastPage = paginationRange[paginationRange.length - 1]const buttonClasses ='flex items-center justify-center h-9 w-9 rounded-md text-sm transition-colors cursor-pointer'const activeClasses = 'bg-white text-black font-medium'const inactiveClasses = 'text-white/60 hover:bg-white/10 hover:text-white'const disabledClasses = 'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-white/60'return (<nav aria-label='Page navigation' {...props}><ul className='flex items-center gap-1'><li><buttononClick={onPrevious}disabled={currentPage === 1}className={`${buttonClasses} ${inactiveClasses} ${disabledClasses}`}><span className='sr-only'>Previous</span><ChevronLeftIcon className='h-4 w-4' /></button></li>{paginationRange.map((pageNumber, index) => {if (pageNumber === DOTS) {return (<likey={index}className='flex items-center justify-center h-9 w-9 text-white/60'>…</li>)}const isActive = currentPage === pageNumberreturn (<li key={index}><buttononClick={() => handlePageChange(pageNumber)}className={`${buttonClasses} ${isActive ? activeClasses : inactiveClasses}`}>{pageNumber}</button></li>)})}<li><buttononClick={onNext}disabled={currentPage === lastPage}className={`${buttonClasses} ${inactiveClasses} ${disabledClasses}`}><span className='sr-only'>Next</span><ChevronRightIcon className='h-4 w-4' /></button></li></ul></nav>)}
