Shadcn Pagination
Free shadcn/ui pagination for React — previous/next, numbered links, and an ellipsis. With the page-range truncation logic, a controlled pager, a compact variant, a data-table footer, and SEO-friendly links.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
No props documented yet.
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}
Examples
Basic
The canonical pagination — previous/next with numbered links and an ellipsis.
Packages
Props
No props documented yet.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationDemo01() {
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
Truncated with ellipsis
Smart range truncation for many pages — a `getPages` helper inserts leading/trailing ellipses.
Packages
Props
No props documented yet.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
// Compute the page list with leading/trailing ellipses for a large total.
function getPages(current: number, total: number) {
const pages: (number | "ellipsis")[] = [1]
if (current > 3) pages.push("ellipsis")
for (
let p = Math.max(2, current - 1);
p <= Math.min(total - 1, current + 1);
p++
) {
pages.push(p)
}
if (current < total - 2) pages.push("ellipsis")
if (total > 1) pages.push(total)
return pages
}
export default function PaginationEllipsis01() {
const current = 6
const total = 20
const pages = getPages(current, total)
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
{pages.map((page, i) => (
<PaginationItem key={i}>
{page === "ellipsis" ? (
<PaginationEllipsis />
) : (
<PaginationLink href="#" isActive={page === current}>
{page}
</PaginationLink>
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
Controlled
A working pager driven by `useState`, with prev/next disabled at the bounds.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationControlled01() {
const total = 5
const [page, setPage] = React.useState(1)
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
setPage((p) => Math.max(1, p - 1))
}}
className={cn(page === 1 && "pointer-events-none opacity-50")}
/>
</PaginationItem>
{Array.from({ length: total }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink
href="#"
isActive={page === i + 1}
onClick={(e) => {
e.preventDefault()
setPage(i + 1)
}}
>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
setPage((p) => Math.min(total, p + 1))
}}
className={cn(page === total && "pointer-events-none opacity-50")}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
Compact
A minimal pager — just Previous / Page X of Y / Next.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationCompact01() {
const total = 10
const [page, setPage] = React.useState(1)
return (
<Pagination>
<PaginationContent className="gap-3">
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
setPage((p) => Math.max(1, p - 1))
}}
className={cn(page === 1 && "pointer-events-none opacity-50")}
/>
</PaginationItem>
<PaginationItem>
<span className="text-muted-foreground px-2 text-sm">
Page {page} of {total}
</span>
</PaginationItem>
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
setPage((p) => Math.min(total, p + 1))
}}
className={cn(page === total && "pointer-events-none opacity-50")}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
Table footer
The data-table bar — a rows-per-page Select with first/prev/next/last icon buttons.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export default function PaginationTable01() {
const totalRows = 97
const [pageSize, setPageSize] = React.useState("10")
const [page, setPage] = React.useState(1)
const totalPages = Math.ceil(totalRows / Number(pageSize))
return (
<div className="flex w-full max-w-2xl flex-col items-center gap-4 sm:flex-row sm:justify-between">
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Rows per page</span>
<Select
value={pageSize}
onValueChange={(v) => {
setPageSize(v)
setPage(1)
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{["10", "20", "50"].map((s) => (
<SelectItem key={s} value={s}>
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="text-muted-foreground">
Page {page} of {totalPages}
</span>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="size-8"
disabled={page === 1}
onClick={() => setPage(1)}
>
<ChevronsLeft />
<span className="sr-only">First page</span>
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
disabled={page === 1}
onClick={() => setPage((p) => p - 1)}
>
<ChevronLeft />
<span className="sr-only">Previous page</span>
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
disabled={page === totalPages}
onClick={() => setPage((p) => p + 1)}
>
<ChevronRight />
<span className="sr-only">Next page</span>
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
disabled={page === totalPages}
onClick={() => setPage(totalPages)}
>
<ChevronsRight />
<span className="sr-only">Last page</span>
</Button>
</div>
</div>
</div>
)
}
SEO links
Real anchor links (`?page=N`) for server-rendered, crawlable pagination.
Packages
Props
No props documented yet.
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
// Real anchor links (?page=N) for server-rendered, SEO-friendly pagination.
export default function PaginationLinks01() {
const current = 2
const total = 5
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href={`?page=${current - 1}`} />
</PaginationItem>
{Array.from({ length: total }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink href={`?page=${i + 1}`} isActive={current === i + 1}>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext href={`?page=${current + 1}`} />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
With results count
A search-results footer — a "Showing X–Y of Z" summary beside a pager.
Packages
Props
No props documented yet.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
export default function PaginationResults01() {
const current = 2
const pageSize = 10
const totalItems = 97
const start = (current - 1) * pageSize + 1
const end = Math.min(current * pageSize, totalItems)
return (
<div className="flex w-full max-w-2xl flex-col items-center gap-3 sm:flex-row sm:justify-between">
<p className="text-muted-foreground text-sm">
Showing {start}–{end} of {totalItems} results
</p>
<Pagination className="mx-0 w-auto justify-end">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">10</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}
The Shadcn Pagination is a set of composable controls for paging through results — previous/next, numbered links, and an ellipsis. It is presentational: it renders the buttons (styled with buttonVariants, no dependency), and you supply the page logic. The examples below include that logic — range truncation, a controlled pager, a table footer, and SEO-friendly links.
Anatomy
<Pagination>
<PaginationContent>
<PaginationItem><PaginationPrevious href="#" /></PaginationItem>
<PaginationItem><PaginationLink href="#">1</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink href="#" isActive>2</PaginationLink></PaginationItem>
<PaginationItem><PaginationEllipsis /></PaginationItem>
<PaginationItem><PaginationNext href="#" /></PaginationItem>
</PaginationContent>
</Pagination>
PaginationLink takes isActive (which sets aria-current="page"), and PaginationPrevious / PaginationNext / PaginationEllipsis are styled helpers.
Truncating with an ellipsis
For large totals you don't render every page — show the first, the last, a window around the current page, and ellipses for the gaps:
function getPages(current, total) {
const pages = [1]
if (current > 3) pages.push("ellipsis")
for (let p = Math.max(2, current - 1); p <= Math.min(total - 1, current + 1); p++) pages.push(p)
if (current < total - 2) pages.push("ellipsis")
if (total > 1) pages.push(total)
return pages
}
The Truncated example renders exactly this.
Controlled (client-side)
Track the page in state and update on click, disabling the ends:
const [page, setPage] = React.useState(1)
<PaginationPrevious
href="#"
onClick={(e) => { e.preventDefault(); setPage((p) => Math.max(1, p - 1)) }}
className={cn(page === 1 && "pointer-events-none opacity-50")}
/>
Server pagination & SEO
For crawlable pages, use real links with a page query so each page is its own URL and the server renders the right slice:
<PaginationLink href={`?page=${n}`} isActive={n === current}>{n}</PaginationLink>
PaginationLink already sets aria-current="page" on the active item. The SEO links example uses this.
In a data table
Tables usually want a compact bar — a rows-per-page Select with first / previous / next / last icon buttons and a "Page X of Y" label. The Table footer example is that bar, ready to wire to your table state (or TanStack Table's pagination API).
Accessibility
Pagination is a <nav aria-label="pagination">, the active page carries aria-current="page", and the ellipsis is hidden from assistive tech (aria-hidden) with screen-reader text. Use real hrefs where you can so pages work without JavaScript.