Use Pagination when a collection has addressable pages. It styles links but does not fetch data, calculate page counts, or manage the current page. For client-side table state, see Data Table.
Live preview
"use client";
import * as React from "react";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
const pageCount = 7;
type PageToken = number | "start-ellipsis" | "end-ellipsis";
function getPageTokens(page: number): PageToken[] {
const visible = [...new Set([1, page - 1, page, page + 1, pageCount])]
.filter((value) => value >= 1 && value <= pageCount)
.sort((a, b) => a - b);
const tokens: PageToken[] = [];
visible.forEach((value, index) => {
const previous = visible[index - 1];
if (index > 0 && value - previous > 1) {
tokens.push(previous === 1 ? "start-ellipsis" : "end-ellipsis");
}
tokens.push(value);
});
return tokens;
}
export default function PaginationDemo() {
const [page, setPage] = React.useState(2);
const tokens = getPageTokens(page);
const selectPage = (event: React.MouseEvent<HTMLAnchorElement>, nextPage: number) => {
event.preventDefault();
setPage(nextPage);
window.history.replaceState(window.history.state, "", `?page=${nextPage}`);
};
return (
<Pagination aria-label={`Pagination, page ${page} of ${pageCount}`}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
text="Prev"
href={`?page=${Math.max(1, page - 1)}`}
aria-disabled={page === 1}
tabIndex={page === 1 ? -1 : undefined}
onClick={(event) => selectPage(event, Math.max(1, page - 1))}
/>
</PaginationItem>
{tokens.map((token) =>
typeof token === "number" ? (
<PaginationItem key={token}>
<PaginationLink
href={`?page=${token}`}
isActive={page === token}
aria-label={`Go to page ${token}`}
className={
token !== 1 && token !== page && token !== pageCount
? "hidden sm:inline-flex"
: undefined
}
onClick={(event) => selectPage(event, token)}
>
{token}
</PaginationLink>
</PaginationItem>
) : (
<PaginationItem key={token} className="hidden sm:block">
<PaginationEllipsis />
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
href={`?page=${Math.min(pageCount, page + 1)}`}
aria-disabled={page === pageCount}
tabIndex={page === pageCount ? -1 : undefined}
onClick={(event) => selectPage(event, Math.min(pageCount, page + 1))}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/pagination.jsonCreate components/ui/pagination.tsx:
import * as React from "react";
import { cn } from "@/lib/utils";
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
const paginationLinkClassName =
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-base border-2 border-border bg-main text-sm font-base text-main-foreground transition-all focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
const paginationLinkSizeClassNames = {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "size-10",
};
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({ className, ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" className={className} {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
size?: "default" | "sm" | "lg" | "icon";
} & React.ComponentProps<"a">;
function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) {
return (
<a
data-slot="pagination-link"
aria-current={isActive ? "page" : undefined}
className={cn(
paginationLinkClassName,
paginationLinkSizeClassNames[size],
className,
isActive &&
"bg-foreground font-heading text-background underline decoration-2 underline-offset-4",
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
text = "Previous",
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
data-slot="pagination-previous"
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeftIcon className="size-4" />
<span>{text}</span>
</PaginationLink>
);
}
function PaginationNext({
className,
text = "Next",
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
data-slot="pagination-next"
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>{text}</span>
<ChevronRightIcon className="size-4" />
</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 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<MoreHorizontalIcon />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
#Usage
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
} from "@/components/ui/pagination";
export default function ArticlePages() {
return (
<Pagination aria-label="Article pages">
<PaginationContent>
<PaginationItem>
<PaginationLink href="?page=1" isActive>
1
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=2" />
</PaginationItem>
</PaginationContent>
</Pagination>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
Pagination | Named navigation landmark | Labelled pagination |
PaginationLink.href | Destination URL | Unset |
PaginationLink.isActive | Current-page state and aria-current | false |
PaginationLink.size | Button-style size: default, sm, lg, or icon | icon |
PaginationPrevious.text / PaginationNext.text | Directional label | Previous / Next |
PaginationEllipsis | Omitted pages | Non-interactive |
#Accessibility
Provide actual destinations and mark exactly one current page. Anchors have no native disabled state: omit unavailable Previous/Next links or render non-interactive text instead of keeping a clickable link styled as disabled. Use a specific collection label when a page has multiple pagination controls.