Pagination

A pagination component with built-in logic - just pass in simple configurations to get a perfect pagination component

Usage

Install the Shadcn UI Button component and Lucide Icons.

Copy the component to your project.

 
'use client'
 
import { useEffect, useMemo, useState } from 'react'
import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
  MoreHorizontal,
} from 'lucide-react'
 
import { Button, ButtonProps } from '@/components/ui/button'
import { cn } from '@/lib/utils'
 
export interface PaginationProps {
  totalPages: number
  currentPage?: number
  onPageChange?: (page: number) => void
  defaultCurrentPage?: number
}
 
const isNil = (value: any): value is null | undefined => value === null || value === undefined
 
const PaginationContainer = ({ children }: { children: React.ReactNode }) => (
  <div className="flex items-center gap-1">{children}</div>
)
 
const PaginationItem = ({
  children,
  isActive,
  ...props
}: ButtonProps & {
  isActive?: boolean
}) => (
  <Button
    {...props}
    className={cn('flex h-10 w-10 items-center justify-center p-0', props.className)}
    variant={isActive ? 'outline' : 'ghost'}
  >
    {children}
  </Button>
)
 
const PaginationEllipsis = ({
  hoverIcon,
  onClick,
}: {
  hoverIcon?: React.ReactNode
  onClick?: React.MouseEventHandler<HTMLButtonElement>
}) => (
  <PaginationItem className="group relative hover:bg-transparent" variant="link" onClick={onClick}>
    <MoreHorizontal
      className={cn(
        'h-4 w-4 transition-opacity duration-200',
        !isNil(hoverIcon) && 'group-hover:opacity-0'
      )}
    />
    {!isNil(hoverIcon) && (
      <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
        {hoverIcon}
      </div>
    )}
    <span className="sr-only">More pages</span>
  </PaginationItem>
)
 
const Pagination = ({
  totalPages,
  currentPage,
  onPageChange,
  defaultCurrentPage,
}: PaginationProps) => {
  const [cmpCurrentPage, setCmpCurrentPage] = useState(defaultCurrentPage ?? currentPage ?? 1)
 
  const handlePageChange = (page: number) => {
    if (isNil(currentPage)) {
      setCmpCurrentPage(page)
    }
    onPageChange?.(page)
  }
 
  const formatPage = (page: number) => {
    if (page < 1) {
      return 1
    } else if (page > totalPages) {
      return totalPages
    }
    return page
  }
 
  const showPreEllipsis = useMemo(
    () => totalPages > 3 && cmpCurrentPage >= 3,
    [totalPages, cmpCurrentPage]
  )
 
  const showNextEllipsis = useMemo(
    () => totalPages > 3 && cmpCurrentPage <= totalPages - 2,
    [totalPages, cmpCurrentPage]
  )
 
  const showPages = useMemo(() => {
    if (cmpCurrentPage === 1) {
      return [1, 2, 3]
    } else if (cmpCurrentPage === totalPages) {
      return [totalPages - 2, totalPages - 1, totalPages]
    } else {
      return [cmpCurrentPage - 1, cmpCurrentPage, cmpCurrentPage + 1]
    }
  }, [totalPages, cmpCurrentPage])
 
  useEffect(() => {
    setCmpCurrentPage(currentPage ?? defaultCurrentPage ?? 1)
  }, [currentPage])
 
  return (
    <PaginationContainer>
      <PaginationItem
        onClick={() => {
          if (cmpCurrentPage === 1) {
            return
          }
          handlePageChange(cmpCurrentPage - 1)
        }}
        disabled={cmpCurrentPage === 1}
      >
        <ChevronLeft className="h-4 w-4" />
      </PaginationItem>
 
      {showPreEllipsis && (
        <PaginationEllipsis
          hoverIcon={<ChevronsLeft className="h-4 w-4" />}
          onClick={() => {
            handlePageChange(formatPage(cmpCurrentPage - 3))
          }}
        />
      )}
 
      {showPages.map(pageNum => (
        <PaginationItem
          key={pageNum}
          isActive={pageNum === cmpCurrentPage}
          onClick={() => {
            if (pageNum === totalPages) {
              return
            }
            handlePageChange(pageNum)
          }}
        >
          {pageNum}
        </PaginationItem>
      ))}
 
      {showNextEllipsis && (
        <PaginationEllipsis
          hoverIcon={<ChevronsRight className="h-4 w-4" />}
          onClick={() => {
            handlePageChange(formatPage(cmpCurrentPage + 3))
          }}
        />
      )}
 
      <PaginationItem
        onClick={() => {
          if (cmpCurrentPage === totalPages) {
            return
          }
          handlePageChange(cmpCurrentPage + 1)
        }}
        disabled={cmpCurrentPage === totalPages}
      >
        <ChevronRight className="h-4 w-4" />
      </PaginationItem>
    </PaginationContainer>
  )
}
 
Pagination.displayName = 'Pagination'
 
export default Pagination

Props

interface PaginationProps {
  totalPages: number // total pages
  currentPage?: number // current page
  onPageChange?: (page: number) => void // callback when page changes
  defaultCurrentPage?: number // default current page
}