Pagination
A shadcn-style pagination component built with Ark UI primitives.
import { ChevronLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationItems,
PaginationNextTrigger,
PaginationPrevTrigger,
} from "@/components/ui/pagination";
const PaginationLinksDemo = () => (
<Pagination
count={60}
getPageUrl={({ page }) => `#pagination-page-${page}`}
pageSize={10}
type="link"
>
<div className="flex flex-wrap items-center gap-1">
<PaginationPrevTrigger asChild>
<Button variant="outline">
<ChevronLeftIcon className="size-4" />
Previous
</Button>
</PaginationPrevTrigger>
<PaginationItems />
<PaginationNextTrigger asChild>
<Button variant="outline">Next</Button>
</PaginationNextTrigger>
</div>
</Pagination>
);
export default PaginationLinksDemo;
Installation
npx shadcn@latest add @ark-cn/paginationInstall the dependency required by this primitive:
npm install @ark-ui/react class-variance-authorityCopy the component source into your app:
TSXcomponents/ui/pagination.tsx
"use client";
import { Pagination as PaginationPrimitive } from "@ark-ui/react/pagination";
import type { VariantProps } from "class-variance-authority";
import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export type PaginationProps = PaginationPrimitive.RootProps;
export const Pagination = ({ className, ...props }: PaginationProps) => (
<PaginationPrimitive.Root
className={cn("flex flex-wrap items-center gap-1.5", className)}
data-slot="pagination"
{...props}
/>
);
export type PaginationRootProviderProps = PaginationPrimitive.RootProviderProps;
export const PaginationRootProvider = ({
...props
}: PaginationRootProviderProps) => (
<PaginationPrimitive.RootProvider
data-slot="pagination-root-provider"
{...props}
/>
);
export type PaginationPrevTriggerProps = PaginationPrimitive.PrevTriggerProps;
export const PaginationPrevTrigger = ({
...props
}: PaginationPrevTriggerProps) => (
<PaginationPrimitive.PrevTrigger
data-slot="pagination-prev-trigger"
{...props}
/>
);
export type PaginationNextTriggerProps = PaginationPrimitive.NextTriggerProps;
export const PaginationNextTrigger = ({
...props
}: PaginationNextTriggerProps) => (
<PaginationPrimitive.NextTrigger
data-slot="pagination-next-trigger"
{...props}
/>
);
export type PaginationFirstTriggerProps = PaginationPrimitive.FirstTriggerProps;
export const PaginationFirstTrigger = ({
...props
}: PaginationFirstTriggerProps) => (
<PaginationPrimitive.FirstTrigger
data-slot="pagination-first-trigger"
{...props}
/>
);
export type PaginationLastTriggerProps = PaginationPrimitive.LastTriggerProps;
export const PaginationLastTrigger = ({
...props
}: PaginationLastTriggerProps) => (
<PaginationPrimitive.LastTrigger
data-slot="pagination-last-trigger"
{...props}
/>
);
export type PaginationItemProps = PaginationPrimitive.ItemProps;
export const PaginationItem = ({ ...props }: PaginationItemProps) => (
<PaginationPrimitive.Item data-slot="pagination-item" {...props} />
);
export type PaginationEllipsisProps = PaginationPrimitive.EllipsisProps;
export const PaginationEllipsis = ({
className,
children,
...props
}: PaginationEllipsisProps) => (
<PaginationPrimitive.Ellipsis
className={cn(
"inline-flex h-9 min-w-9 items-center justify-center px-1 text-muted-foreground text-sm sm:h-8",
className,
)}
data-slot="pagination-ellipsis"
{...props}
>
{children ? children : "…"}
</PaginationPrimitive.Ellipsis>
);
export type PaginationContextProps = PaginationPrimitive.ContextProps;
export const PaginationContext = (props: PaginationContextProps) => (
<PaginationPrimitive.Context {...props} />
);
export type PaginationItemsProps = VariantProps<typeof buttonVariants> & {
itemType?: "button" | "link";
};
export const PaginationItems = ({
size = "icon",
variant = "ghost",
itemType = "button",
}: PaginationItemsProps) => {
return (
<PaginationContext>
{(pagination) =>
pagination.pages.map((page, index) =>
page.type === "page" ? (
<PaginationItem key={index} {...page} asChild>
{itemType === "button" ? (
<Button
size={size}
variant={
pagination?.page === page.value ? "outline" : variant
}
>
{page.value}
</Button>
) : (
<a
key={index}
className={buttonVariants({ size, variant })}
{...pagination.getItemProps(page)}
>
{page.value}
</a>
)}
</PaginationItem>
) : (
<PaginationEllipsis key={index} index={index} />
),
)
}
</PaginationContext>
);
};
export type {
PaginationItemLabelDetails,
PaginationPageChangeDetails,
PaginationPageSizeChangeDetails,
PaginationPageUrlDetails,
} from "@ark-ui/react/pagination";
export {
type UsePaginationContext,
type UsePaginationProps,
type UsePaginationReturn,
usePagination,
usePaginationContext,
} from "@ark-ui/react/pagination";
Update import aliases to match your project setup.
Usage
import * as Pagination from "@/components/ui/pagination"Read exported parts in src/components/ui/pagination.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Context Info
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationContext,
PaginationItems,
PaginationNextTrigger,
PaginationPrevTrigger,
} from "@/components/ui/pagination";
const PaginationContextInfoDemo = () => (
<Pagination count={55} pageSize={10}>
<PaginationContext>
{(api) => (
<div className="flex flex-col gap-2">
<p className="rounded-md border border-border/80 bg-muted/40 px-2 py-1.5 text-foreground text-xs">
pageRange: {api.pageRange.start}–{api.pageRange.end} of {api.count}{" "}
· totalPages: {api.totalPages}
</p>
<div className="flex flex-wrap items-center gap-1">
<PaginationPrevTrigger asChild>
<Button size="icon" variant="outline">
<ChevronLeftIcon className="size-4" />
</Button>
</PaginationPrevTrigger>
<PaginationItems />
<PaginationNextTrigger asChild>
<Button size="icon" variant="outline">
<ChevronRightIcon className="size-4" />
</Button>
</PaginationNextTrigger>
</div>
</div>
)}
</PaginationContext>
</Pagination>
);
export default PaginationContextInfoDemo;
Controlled
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationContext,
PaginationItems,
PaginationNextTrigger,
PaginationPrevTrigger,
} from "@/components/ui/pagination";
const PaginationControlledDemo = () => {
const [page, setPage] = useState(1);
return (
<Pagination
count={120}
page={page}
pageSize={10}
onPageChange={(d) => setPage(d.page)}
>
<PaginationContext>
{(api) => (
<div className="flex flex-col gap-2">
<p className="text-muted-foreground text-xs">
Controlled page:{" "}
<span className="font-medium text-foreground">{page}</span>{" "}
(synced with api: {api.page})
</p>
<div className="flex flex-wrap items-center gap-1">
<PaginationPrevTrigger asChild>
<Button size="icon" variant="outline">
<ChevronLeftIcon className="size-4" />
</Button>
</PaginationPrevTrigger>
<PaginationItems />
<PaginationNextTrigger asChild>
<Button size="icon" variant="outline">
<ChevronRightIcon className="size-4" />
</Button>
</PaginationNextTrigger>
</div>
</div>
)}
</PaginationContext>
</Pagination>
);
};
export default PaginationControlledDemo;
Default
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationItems,
PaginationNextTrigger,
PaginationPrevTrigger,
} from "@/components/ui/pagination";
const PaginationDefaultDemo = () => (
<Pagination
count={100}
pageSize={10}
className="flex flex-wrap items-center gap-1"
>
<PaginationPrevTrigger asChild aria-label="Previous page">
<Button size="icon" variant="outline">
<ChevronLeftIcon className="size-4" />
</Button>
</PaginationPrevTrigger>
<PaginationItems />
<PaginationNextTrigger asChild aria-label="Next page">
<Button size="icon" variant="outline">
<ChevronRightIcon className="size-4" />
</Button>
</PaginationNextTrigger>
</Pagination>
);
export default PaginationDefaultDemo;
Links
import { ChevronLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationItems,
PaginationNextTrigger,
PaginationPrevTrigger,
} from "@/components/ui/pagination";
const PaginationLinksDemo = () => (
<Pagination
count={60}
getPageUrl={({ page }) => `#page-${page}`}
pageSize={10}
type="link"
>
<div className="flex flex-wrap items-center gap-1">
<PaginationPrevTrigger asChild>
<Button variant="outline">
<ChevronLeftIcon className="size-4" />
Previous
</Button>
</PaginationPrevTrigger>
<PaginationItems itemType="link" />
<PaginationNextTrigger asChild>
<Button variant="outline">Next</Button>
</PaginationNextTrigger>
</div>
</Pagination>
);
export default PaginationLinksDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
PaginationItems
| Prop | Type | Description |
|---|---|---|
| itemType? | "button" | "link" | Renders page items as Button or styled anchor. |
| size? | ButtonSize | Button size for page controls. |
| variant? | ButtonVariant | Button variant for page controls (current page forces outline). |
See the ARK UI documentation for the full API.