64 / Wayfinding
Pagination
Page numbers with gaps where the run is broken, as buttons or as your own links.
Page 7 of 20
"use client"
import * as React from "react"
import { Pagination } from "mischief-ui/pagination"
export function PaginationDemo() {
const [page, setPage] = React.useState(7)
return (
<div className="grid justify-items-center gap-4">
<Pagination page={page} pageCount={20} onPageChange={setPage} />
<p className="text-muted-foreground text-xs" role="status">
Page {page} of 20
</p>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/paginationimport { Pagination } from "mischief-ui/pagination"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
import * as React from "react"import { ChevronLeft, ChevronRight } from "lucide-react" import { cn } from "@/lib/utils" export type PaginationLink = { page: number children: React.ReactNode "aria-label"?: string "aria-current"?: "page" className: string} export type PaginationProps = Omit<Usage
export function Results({ page, pageCount }) {
return (
<Pagination
page={page}
pageCount={pageCount}
onPageChange={setPage}
/>
)
}Buttons, or addresses
Buttons suit a list whose page lives in component state. Where the page belongs in the URL -- and on a page anyone might share, bookmark, or let a search engine index, it does -- render real links instead. renderLink hands you the page number and the classes, and you decide what an anchor to it looks like.
<Pagination
page={page}
pageCount={pageCount}
renderLink={({ page, children, className, ...rest }) => (
<Link href={`?page=${page}`} className={className} {...rest}>
{children}
</Link>
)}
/>Where the gaps fall
Ends are always shown, the current page keeps siblingCount neighbours, and everything between collapses to an ellipsis. A gap costs a slot of its own, so a run short enough to draw whole is drawn whole rather than replaced by something no shorter.
paginationRange is exported, so the same numbers can be worked out without rendering anything -- for a summary line, or for a test.
paginationRange({ page: 7, pageCount: 20 })
// [1, "gap", 6, 7, 8, "gap", 20]API
pagenumberThe current page, counting from one.pageCountnumberHow many there are. One or fewer renders nothing.onPageChange(page: number) => voidCalled with the page that was chosen.siblingCountnumberPages either side of the current one. Defaults to 1.boundaryCountnumberPages kept at each end. Defaults to 1.renderLink(link: PaginationLink) => ReactNodeRenders every page as your own link.labelstringNames the navigation. Defaults to "Pagination".Accessibility
A navigation landmark with a name, so it can be jumped to and told apart from other navigation on the page. Every page is named in full rather than by its digit alone, and the current one carries aria-current so it is announced as where you are rather than as somewhere to go. The ellipsis is decoration and hidden. Previous and next are absent at the ends rather than present and disabled.