Pagination
Page links with previous and next, for a list too long for one page.
Each page is a link: an anchor wearing Vibra's button recipe — ghost, with the current page outlined and marked aria-current="page" — where shadcn's base-nova renders Base UI's Button as the anchor, which gave every page role="button" and left a screen reader announcing a row of buttons. Previous and Next turn their arrows round and pad the arrow's own side in a right-to-left row. For a table's pager with a page size and a count, DataTable brings its own.
Install
npx shadcn@latest add @vibra/paginationNeeds the @vibra registry in your components.json — set it up once.
Examples
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 12
/** The pages worth a link: the ends, and the neighbours of the current one. */
function visiblePages(current: number): (number | "gap")[] {
const pages = new Set([1, current - 1, current, current + 1, PAGES])
const sorted = [...pages].filter((page) => page >= 1 && page <= PAGES).sort((a, b) => a - b)
return sorted.flatMap((page, index) => (index > 0 && page - sorted[index - 1] > 1 ? ["gap" as const, page] : [page]))
}
export default function PaginationDemo() {
const [page, setPage] = React.useState(4)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
return (
<div className="flex flex-col items-center gap-3">
<Pagination aria-label="Invoice pages">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" onClick={go(page - 1)} aria-disabled={page === 1} />
</PaginationItem>
{visiblePages(page).map((entry, index) =>
entry === "gap" ? (
<PaginationItem key={`gap-${index}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={entry}>
<PaginationLink href="#" isActive={entry === page} onClick={go(entry)}>
{entry}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext href="#" onClick={go(page + 1)} aria-disabled={page === PAGES} />
</PaginationItem>
</PaginationContent>
</Pagination>
<p className="text-xs text-muted-foreground tabular-nums">
Invoices {(page - 1) * 25 + 1}–{Math.min(page * 25, 289)} of 289
</p>
</div>
)
}Previous and next only
A log paged by cursor has no total, so no numbers: newer and older at either end, the one with nothing past it marked unavailable.
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
// The event log pages by cursor: the API hands back "newer" and "older" and
// never a total, so there are no page numbers to offer. The two links sit at
// either end, and the end with nothing past it is marked unavailable rather
// than removed, so the row doesn't jump.
const PAGES = ["Sep 24, 09:12 – 14:40", "Sep 23, 16:05 – Sep 24, 09:10", "Sep 22, 11:30 – Sep 23, 16:02"]
export default function PaginationPrevNext() {
const [page, setPage] = React.useState(0)
const newest = page === 0
const oldest = page === PAGES.length - 1
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES.length - 1, Math.max(0, next)))
}
return (
<div className="flex w-full max-w-md flex-col gap-3">
<p className="text-center text-sm text-muted-foreground tabular-nums">Events {PAGES[page]}</p>
<Pagination aria-label="Event log pages">
<PaginationContent className="w-full justify-between">
<PaginationItem>
<PaginationPrevious href="#" text="Newer" aria-label="Newer events" aria-disabled={newest || undefined} onClick={go(page - 1)} />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" text="Older" aria-label="Older events" aria-disabled={oldest || undefined} onClick={go(page + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}Page counter
"Page 3 of 12" between two named arrows, as a polite status, where a row of numbers would crowd a card.
"use client"
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import { Pagination, PaginationContent, PaginationItem, PaginationLink } from "@/components/ui/pagination"
const PAGES = 12
// Where a row of numbers would crowd a card, a counter between two arrows
// says the same thing. The arrows are named, since they carry no words, and
// the counter is a polite status, so moving a page is heard as well as seen.
export default function PaginationCounter() {
const [page, setPage] = React.useState(3)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
return (
<Pagination aria-label="Customer pages">
<PaginationContent className="gap-2">
<PaginationItem>
<PaginationLink href="#" aria-label="Previous page" aria-disabled={page === 1 || undefined} onClick={go(page - 1)}>
<ChevronLeftIcon aria-hidden="true" className="rtl:rotate-180" />
</PaginationLink>
</PaginationItem>
<PaginationItem className="min-w-24 text-center text-sm text-muted-foreground tabular-nums">
<span role="status">
Page <span className="font-medium text-foreground">{page}</span> of {PAGES}
</span>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" aria-label="Next page" aria-disabled={page === PAGES || undefined} onClick={go(page + 1)}>
<ChevronRightIcon aria-hidden="true" className="rtl:rotate-180" />
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}First and last page
480 pages of audit log: the ends are one click away, and on a phone the neighbours step aside.
"use client"
import * as React from "react"
import { ChevronsLeftIcon, ChevronsRightIcon } from "lucide-react"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 480
// 480 pages of audit log: the ends are a click away, not 240. The first and
// last links are named for where they go, and a link that would go nowhere
// says it is unavailable instead of disappearing.
export default function PaginationFirstLast() {
const [page, setPage] = React.useState(240)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
const first = page === 1
const last = page === PAGES
const middle = [page - 1, page, page + 1].filter((n) => n > 1 && n < PAGES)
return (
<Pagination aria-label="Audit log pages">
<PaginationContent>
<PaginationItem>
<PaginationLink href="#" aria-label="First page" aria-disabled={first || undefined} onClick={go(1)}>
<ChevronsLeftIcon aria-hidden="true" className="rtl:rotate-180" />
</PaginationLink>
</PaginationItem>
<PaginationItem className="max-sm:hidden">
<PaginationPrevious href="#" aria-disabled={first || undefined} onClick={go(page - 1)} />
</PaginationItem>
{[1, ...middle, PAGES].map((n, index, all) => (
<React.Fragment key={n}>
{index > 0 && n - all[index - 1] > 1 ? (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
) : null}
{/* On a phone the neighbours step aside: first, current, last. */}
<PaginationItem className={Math.abs(n - page) === 1 && n !== 1 && n !== PAGES ? "max-sm:hidden" : undefined}>
<PaginationLink href="#" isActive={n === page} onClick={go(n)} className="w-auto min-w-8 px-1.5 tabular-nums">
{n}
</PaginationLink>
</PaginationItem>
</React.Fragment>
))}
<PaginationItem className="max-sm:hidden">
<PaginationNext href="#" aria-disabled={last || undefined} onClick={go(page + 1)} />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" aria-label="Last page" aria-disabled={last || undefined} onClick={go(PAGES)}>
<ChevronsRightIcon aria-hidden="true" className="rtl:rotate-180" />
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Compact on a phone
Below sm the numbers give way to "4 of 12"; one of the two shows at a time, so the current page is met once.
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 12
// On a phone the numbers give way to "4 of 12" between the arrows; from sm up
// the numbers come back. Only one of the two is ever shown, so a screen
// reader meets the current page once at either width.
export default function PaginationCompact() {
const [page, setPage] = React.useState(4)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
const numbers = [1, page - 1, page, page + 1, PAGES].filter((n, i, all) => n >= 1 && n <= PAGES && all.indexOf(n) === i)
return (
<Pagination aria-label="Report pages">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" aria-disabled={page === 1 || undefined} onClick={go(page - 1)} />
</PaginationItem>
<PaginationItem className="px-2 text-sm text-muted-foreground tabular-nums sm:hidden">
{page} of {PAGES}
</PaginationItem>
{numbers.map((n, index) => (
<React.Fragment key={n}>
{index > 0 && n - numbers[index - 1] > 1 ? (
<PaginationItem className="max-sm:hidden">
<PaginationEllipsis />
</PaginationItem>
) : null}
<PaginationItem className="max-sm:hidden">
<PaginationLink href="#" isActive={n === page} onClick={go(n)}>
{n}
</PaginationLink>
</PaginationItem>
</React.Fragment>
))}
<PaginationItem>
<PaginationNext href="#" aria-disabled={page === PAGES || undefined} onClick={go(page + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}With rows per page
An invoices table's footer: the page size, the range shown, and the arrows. A new size goes back to the first page.
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
const TOTAL = 289
const SIZES = [10, 25, 50, 100].map((size) => ({ value: String(size), label: String(size) }))
// The footer of an invoices table: how many rows a page holds, which rows
// these are, and the way to the next ones. A new page size goes back to the
// first page, so the range never points past the end.
export default function PaginationRows() {
const [size, setSize] = React.useState(25)
const [page, setPage] = React.useState(2)
const pages = Math.ceil(TOTAL / size)
const from = (page - 1) * size + 1
const to = Math.min(page * size, TOTAL)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(pages, Math.max(1, next)))
}
return (
<div className="panel flex w-full max-w-xl flex-wrap items-center justify-between gap-x-6 gap-y-3 px-3 py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span id="pagination-rows-label">Rows per page</span>
<Select
items={SIZES}
value={String(size)}
onValueChange={(value) => {
setSize(Number(value))
setPage(1)
}}
>
<SelectTrigger size="sm" aria-labelledby="pagination-rows-label" className="tabular-nums">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SIZES.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-3">
<p role="status" className="text-sm text-muted-foreground tabular-nums">
{from}–{to} of {TOTAL}
</p>
<Pagination aria-label="Invoice pages" className="mx-0 w-auto">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" aria-disabled={page === 1 || undefined} onClick={go(page - 1)} />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" aria-disabled={page === pages || undefined} onClick={go(page + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
)
}Jump to a page
A typed page number, refused where it was typed when it doesn't exist, with the range and the focus kept in the field.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 48
// A reader who knows the order is on page 31 types it. A page that doesn't
// exist is refused where it was typed, with the range, and the field keeps
// the focus so the number can be fixed.
export default function PaginationJump() {
const [page, setPage] = React.useState(1)
const [error, setError] = React.useState("")
function jump(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const input = event.currentTarget.elements.namedItem("page") as HTMLInputElement
const next = Number(input.value)
if (!Number.isInteger(next) || next < 1 || next > PAGES) {
setError(`Pages run from 1 to ${PAGES}.`)
input.focus()
return
}
setError("")
setPage(next)
}
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
return (
<div className="flex w-full max-w-md flex-wrap items-start justify-between gap-3">
<Pagination aria-label="Order pages" className="mx-0 w-auto">
<PaginationContent className="gap-2">
<PaginationItem>
<PaginationPrevious href="#" aria-disabled={page === 1 || undefined} onClick={go(page - 1)} />
</PaginationItem>
<PaginationItem className="text-sm text-muted-foreground tabular-nums">
<span role="status">
{page} of {PAGES}
</span>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" aria-disabled={page === PAGES || undefined} onClick={go(page + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
<form onSubmit={jump} noValidate className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<label htmlFor="pagination-jump" className="text-sm text-muted-foreground">
Go to page
</label>
<Input
id="pagination-jump"
name="page"
inputMode="numeric"
className="w-16 tabular-nums"
aria-invalid={error ? true : undefined}
aria-describedby={error ? "pagination-jump-error" : undefined}
onChange={() => error && setError("")}
/>
<Button type="submit" variant="outline">
Go
</Button>
</div>
{/* The focus is already in the field, so the refusal is spoken as it appears. */}
<p id="pagination-jump-error" role="alert" className="min-h-4 text-xs text-danger">
{error}
</p>
</form>
</div>
)
}Server-rendered links
Every page a real URL with no state at all: it works before any script loads, opens in a new tab, and walks back with the browser.
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 20
const href = (page: number) => `/invoices?page=${page}`
// Rendered on the server from the URL, with no state at all: every page is a
// real link, so it works before any script loads, opens in a new tab, and
// the back button walks the pages. The current page is plain aria-current;
// rel="prev" and rel="next" tell the browser which way the list runs.
export default function PaginationLinks({ page = 7 }: { page?: number }) {
return (
<Pagination aria-label="Invoice pages">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href={href(page - 1)} rel="prev" />
</PaginationItem>
<PaginationItem>
<PaginationLink href={href(1)}>1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
{[page - 1, page, page + 1].map((n) => (
<PaginationItem key={n} className={n === page ? undefined : "max-sm:hidden"}>
<PaginationLink href={href(n)} isActive={n === page}>
{n}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href={href(PAGES)}>{PAGES}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href={href(page + 1)} rel="next" />
</PaginationItem>
</PaginationContent>
</Pagination>
)
}Waits for the page
The rows dim under aria-busy, the pressed link turns into a spinner and keeps the focus, and a second press does nothing.
"use client"
import * as React from "react"
import { ChevronRightIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Spinner } from "@/components/ui/spinner"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 12
const rows = (page: number) => [0, 1, 2].map((i) => `INV-${2040 + page * 3 + i}`)
// The next page comes from the server. Until it lands the rows dim under
// aria-busy, the link that was pressed turns into a spinner and holds the
// focus, and a second press does nothing; then the new rows and "Page 3 of
// 12" arrive together.
export default function PaginationLoading() {
const [page, setPage] = React.useState(2)
const [pending, setPending] = React.useState<number | null>(null)
React.useEffect(() => {
if (pending === null) return
const timer = window.setTimeout(() => {
setPage(pending)
setPending(null)
}, 800)
return () => window.clearTimeout(timer)
}, [pending])
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
if (pending === null && next >= 1 && next <= PAGES) setPending(next)
}
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<ul aria-label="Invoices" aria-busy={pending !== null} className={cn("flex flex-col divide-y divide-border rounded-lg border border-border", pending !== null && "opacity-60")}>
{rows(page).map((id) => (
<li key={id} className="px-3 py-2 font-mono text-sm">
{id}
</li>
))}
</ul>
<Pagination aria-label="Invoice pages">
<PaginationContent className="w-full justify-between">
<PaginationItem>
<PaginationPrevious href="#" aria-disabled={page === 1 || pending !== null || undefined} onClick={go(page - 1)} />
</PaginationItem>
<PaginationItem className="text-sm text-muted-foreground tabular-nums">
<span role="status">
Page {page} of {PAGES}
</span>
</PaginationItem>
<PaginationItem>
<PaginationLink
href="#"
size="default"
aria-label="Go to next page"
aria-disabled={page === PAGES || pending !== null || undefined}
aria-busy={pending !== null || undefined}
onClick={go(page + 1)}
className="pe-1.5!"
>
<span className="hidden sm:block">Next</span>
{pending === null ? <ChevronRightIcon aria-hidden="true" className="rtl:rotate-180" /> : <Spinner className="size-4" />}
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}Right to left
Previous sits on the right and points right, back towards page 1; the arrows' padding follows them.
"use client"
import * as React from "react"
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
const PAGES = 12
// The pages run from the right, so "previous" sits on the right and its arrow
// points right, back towards page 1; the tighter padding stays on the arrow's
// side. The markup keeps its order: it is the reading order.
export default function PaginationRtl() {
const [page, setPage] = React.useState(4)
const go = (next: number) => (event: React.MouseEvent) => {
event.preventDefault()
setPage(Math.min(PAGES, Math.max(1, next)))
}
const numbers = [1, page - 1, page, page + 1, PAGES].filter((n, i, all) => n >= 1 && n <= PAGES && all.indexOf(n) === i)
return (
<div dir="rtl" lang="ar">
<Pagination aria-label="صفحات الفواتير">
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" text="السابق" aria-label="الصفحة السابقة" aria-disabled={page === 1 || undefined} onClick={go(page - 1)} />
</PaginationItem>
{numbers.map((n, index) => (
<React.Fragment key={n}>
{index > 0 && n - numbers[index - 1] > 1 ? (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
) : null}
<PaginationItem className={Math.abs(n - page) === 1 && n !== 1 && n !== PAGES ? "max-sm:hidden" : undefined}>
<PaginationLink href="#" isActive={n === page} onClick={go(n)}>
{n}
</PaginationLink>
</PaginationItem>
</React.Fragment>
))}
<PaginationItem>
<PaginationNext href="#" text="التالي" aria-label="الصفحة التالية" aria-disabled={page === PAGES || undefined} onClick={go(page + 1)} />
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| aria-label | string | "pagination" | Names the navigation. Say which list it pages — "Invoice pages" — when a page has more than one. |
| PaginationLink.isActive | boolean | — | Marks the current page: the outline button and aria-current. |
| PaginationLink.size | "icon" | "default" | "sm" | … | "icon" | The button size the link wears. A three-digit page widens with its className. |
| PaginationPrevious.text / PaginationNext.text | string | "Previous" / "Next" | The words beside the arrow, shown from sm up. Pass an aria-label to match them: "Newer events". |
| aria-disabled | boolean | — | On a link with nowhere to go: it dims and says so, and stays in its place. Stop its click in the handler. |
Dependencies
Registry
npm
Source
import * as React from "react"
import { cn } from "@/lib/utils"
import { buttonVariants, type ButtonProps } from "@/components/ui/button"
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
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 items-center gap-0.5", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
/**
* A page is a place, so each one is a link: an anchor that wears the button
* recipe, the way shadcn's Radix pagination draws it. Rendering Base UI's
* Button as the anchor instead gave every page `role="button"`, and a
* screen reader announced a row of buttons that never said where they went.
*/
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive || undefined}
data-size={size}
className={cn(
buttonVariants({ variant: isActive ? "outline" : "ghost", size }),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
text = "Previous",
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
// The arrow turns and the padding follows it in a right-to-left row,
// where "previous" sits on the right and points right.
className={cn("ps-1.5!", className)}
{...props}
>
<ChevronLeftIcon data-icon="inline-start" className="rtl:rotate-180" />
<span className="hidden sm:block">{text}</span>
</PaginationLink>
)
}
function PaginationNext({
className,
text = "Next",
...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("pe-1.5!", className)}
{...props}
>
<span className="hidden sm:block">{text}</span>
<ChevronRightIcon data-icon="inline-end" className="rtl:rotate-180" />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn(
"flex size-8 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,
}