React Pagination Hook, which is lightweight, fast and easy to use.
https://serhat-m.github.io/pagination-react-js
This is an example of fully customizable pagination:
import { usePagination, generateTestData } from "pagination-react-js"
type PaginationItemProps = {
children: React.ReactNode
label: React.ComponentProps<"button">["aria-label"]
active?: boolean
onClick: React.ComponentProps<"button">["onClick"]
}
const PaginationItem = ({ children, label, active, onClick }: PaginationItemProps) => {
return (
<li>
<button
type="button"
className={["pagination-item", active ? "pagination-item-active" : undefined].filter((value) => value).join(" ")}
onClick={onClick}
aria-current={active ?? "page"}
aria-label={label}
>
{children}
</button>
</li>
)
}
const Pagination = () => {
const dataList = generateTestData(700, (i) => ({
id: `Id${i}`,
name: `Name${i}`,
}))
const { records, pageNumbers, setActivePage, setRecordsPerPage } = usePagination({
activePage: 1,
recordsPerPage: 10,
totalRecordsLength: dataList.length,
offset: 2,
navCustomPageSteps: { prev: 3, next: 3 },
permanentFirstNumber: true,
permanentLastNumber: true,
})
function updateActivePage(pageNumber: number | false) {
pageNumber && setActivePage(pageNumber)
}
return (
<div>
<table>
<tbody>
{dataList.slice(records.indexOfFirst, records.indexOfLast + 1).map((record) => {
return (
<tr key={record.id}>
<td>{record.id}</td>
<td>{record.name}</td>
</tr>
)
})}
</tbody>
</table>
<nav aria-label="Page navigation">
<ul className="pagination">
<PaginationItem label={`Goto first page ${pageNumbers.firstPage}`} onClick={() => updateActivePage(pageNumbers.firstPage)}>
«
</PaginationItem>
<PaginationItem label={`Goto previous page ${pageNumbers.previousPage}`} onClick={() => updateActivePage(pageNumbers.previousPage)}>
‹
</PaginationItem>
<PaginationItem
label={`Goto first page ${pageNumbers.firstPage}`}
active={pageNumbers.firstPage === pageNumbers.activePage}
onClick={() => updateActivePage(pageNumbers.firstPage)}
>
{pageNumbers.firstPage}
</PaginationItem>
{pageNumbers.customPreviousPage && (
<PaginationItem label={`Goto page ${pageNumbers.customPreviousPage}`} onClick={() => updateActivePage(pageNumbers.customPreviousPage)}>
···
</PaginationItem>
)}
{pageNumbers.navigation.map((navigationNumber) => {
const isFirstOrLastPage = navigationNumber === pageNumbers.firstPage || navigationNumber === pageNumbers.lastPage
return isFirstOrLastPage ? null : (
<PaginationItem
label={`Goto page ${navigationNumber}`}
key={navigationNumber}
active={navigationNumber === pageNumbers.activePage}
onClick={() => updateActivePage(navigationNumber)}
>
{navigationNumber}
</PaginationItem>
)
})}
{pageNumbers.customNextPage && (
<PaginationItem label={`Goto page ${pageNumbers.customNextPage}`} onClick={() => updateActivePage(pageNumbers.customNextPage)}>
···
</PaginationItem>
)}
{pageNumbers.firstPage !== pageNumbers.lastPage && (
<PaginationItem
label={`Goto last page ${pageNumbers.lastPage}`}
active={pageNumbers.lastPage === pageNumbers.activePage}
onClick={() => updateActivePage(pageNumbers.lastPage)}
>
{pageNumbers.lastPage}
</PaginationItem>
)}
<PaginationItem label={`Goto next page ${pageNumbers.nextPage}`} onClick={() => updateActivePage(pageNumbers.nextPage)}>
›
</PaginationItem>
<PaginationItem label={`Goto last page ${pageNumbers.lastPage}`} onClick={() => updateActivePage(pageNumbers.lastPage)}>
»
</PaginationItem>
</ul>
</nav>
</div>
)
}
:root {
font-size: 16px;
--border-size: 0.125rem;
--border-radius: 0.3rem;
--color-gray: #e1e4e7;
--color-active: #0a7ea3;
}
.pagination {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0.5rem;
width: fit-content;
list-style-type: none;
padding: 0.5rem 0.625rem;
border: var(--border-size) solid var(--color-gray);
border-radius: var(--border-radius);
user-select: none;
}
.pagination-item {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--border-radius);
}
.pagination-item:hover {
cursor: pointer;
background-color: var(--color-gray);
}
.pagination-item-active {
color: white;
background-color: var(--color-active);
pointer-events: none;
}
type TFnOptions = {
activePage: number // initial active page number
recordsPerPage: number // initial records per page
totalRecordsLength: number // data list length
offset: number // amount of numbers before and after the active number
navCustomPageSteps?: { prev?: number; next?: number } // step size of the user-defined navigation
permanentFirstNumber?: boolean // offset adjustment for permanent first number
permanentLastNumber?: boolean // offset adjustment for permanent last number
}
type TPaginationData = {
readonly records: {
perPage: number // number of records per page
indexOfFirst: number // index of first record
indexOfLast: number // index of last record
}
readonly pageNumbers: {
activePage: number // active page number
firstPage: number // first page number
lastPage: number // last page number
previousPage: number | false // previous page number
nextPage: number | false // next page number
customPreviousPage: number | false // custom previous page number
customNextPage: number | false // custom next page number
navigation: number[] // array of navigation numbers
}
readonly setActivePage: (pageNumber: number) => void // function to update the active page
readonly setRecordsPerPage: (recordsPerPage: number) => void // function to update the records per page
}