Pagination
betaPrevious, next, and numbered page links in a labeled nav, with the current page marked and an ellipsis for gaps.
Preview
first
middle
last
sm
md
lg
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@arbokuiin a public list that Arbokui does not control.npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @arbokui/theme3. Add the component.
npx shadcn@latest add @arbokui/pagination
Use it for
- Moving through a long list split into pages, like search results, orders, or a table.
- Mark the page people are on with current. It sets aria-current="page" and draws an outline.
- Keep the first and last page, the pages beside the current one, and a PaginationEllipsis for each gap.
- Set disabled on Previous on the first page and on Next on the last, so the row keeps its shape.
- Put the page in the URL, like ?page=3, so people can share it and the back button works.
- With a router, pass its link as render: <PaginationLink render={<Link href="?page=2" />}>2</PaginationLink>.
- Set the same size on every link and the ellipsis in one row.
Avoid it for
- Short lists that fit on one screen. Show them all.
- Feeds people scroll through without a goal. A Load more button keeps their place better.
- Steps of a form or a flow. Use StepDots, or Back and Next buttons.
- Showing every page of a long list. Phones wrap the row. Show about seven items at most.
- Two navs named "Pagination" on one page, like above and below a table. Pass a different aria-label to each.
Props
- size
On PaginationLink, PaginationPrevious, PaginationNext, and PaginationEllipsis. Height and width. Density scales them.
"sm" | "md" | "lg" · default "md"
- current
On PaginationLink. The page people are on. Sets aria-current="page" and an outline.
boolean · default false
- disabled
On PaginationLink, PaginationPrevious, and PaginationNext. Drops href and onClick, sets aria-disabled, and dims the link. It is not a tab stop.
boolean · default false
- aria-label
On Pagination. The landmark's name. Rename it when a page has more than one. Left off when you pass aria-labelledby.
string · default "Pagination"
- label
On PaginationEllipsis. What screen readers hear in place of the dots.
string · default "More pages"
- render
On PaginationLink, PaginationPrevious, and PaginationNext. Renders your element, like a router link, with the link's props and classes. A disabled link ignores it and renders an <a>.
ReactElement
- Pagination
The landmark. A <nav> with aria-label="Pagination". Pass aria-label or aria-labelledby to rename it.
Omit<ComponentProps<"nav">, "color">
- PaginationContent
Part. The list of items. It wraps as a last resort, so it never pushes the page sideways.
Omit<ComponentProps<"ul">, "color">
- PaginationItem
Part. One item in the row.
Omit<ComponentProps<"li">, "color">
- PaginationLink
Part. One page, styled with buttonVariants: ghost, or outline when current. At least as wide as it is tall.
Omit<ComponentProps<"a">, "color"> & { current?, disabled?, size?, render? }
- PaginationPrevious
Part. Named "Previous page". Below the sm breakpoint the words are visually hidden and the link turns square.
Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? }
- PaginationNext
Part. Named "Next page". Below the sm breakpoint the words are visually hidden and the link turns square.
Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? }
- PaginationEllipsis
Part. Three dots for pages left out. label is what screen readers hear. Default "More pages".
Omit<ComponentProps<"span">, "children" | "color"> & { label?, size? }
- className
Extra classes on any part. Later classes win, so className='rounded-full' on PaginationLink replaces the radius.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "md" | On PaginationLink, PaginationPrevious, PaginationNext, and PaginationEllipsis. Height and width. Density scales them. |
| current | boolean | false | On PaginationLink. The page people are on. Sets aria-current="page" and an outline. |
| disabled | boolean | false | On PaginationLink, PaginationPrevious, and PaginationNext. Drops href and onClick, sets aria-disabled, and dims the link. It is not a tab stop. |
| aria-label | string | "Pagination" | On Pagination. The landmark's name. Rename it when a page has more than one. Left off when you pass aria-labelledby. |
| label | string | "More pages" | On PaginationEllipsis. What screen readers hear in place of the dots. |
| render | ReactElement | - | On PaginationLink, PaginationPrevious, and PaginationNext. Renders your element, like a router link, with the link's props and classes. A disabled link ignores it and renders an <a>. |
| Pagination | Omit<ComponentProps<"nav">, "color"> | - | The landmark. A <nav> with aria-label="Pagination". Pass aria-label or aria-labelledby to rename it. |
| PaginationContent | Omit<ComponentProps<"ul">, "color"> | - | Part. The list of items. It wraps as a last resort, so it never pushes the page sideways. |
| PaginationItem | Omit<ComponentProps<"li">, "color"> | - | Part. One item in the row. |
| PaginationLink | Omit<ComponentProps<"a">, "color"> & { current?, disabled?, size?, render? } | - | Part. One page, styled with buttonVariants: ghost, or outline when current. At least as wide as it is tall. |
| PaginationPrevious | Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? } | - | Part. Named "Previous page". Below the sm breakpoint the words are visually hidden and the link turns square. |
| PaginationNext | Omit<ComponentProps<"a">, "color"> & { disabled?, size?, render? } | - | Part. Named "Next page". Below the sm breakpoint the words are visually hidden and the link turns square. |
| PaginationEllipsis | Omit<ComponentProps<"span">, "children" | "color"> & { label?, size? } | - | Part. Three dots for pages left out. label is what screen readers hear. Default "More pages". |
| className | string | - | Extra classes on any part. Later classes win, so className='rounded-full' on PaginationLink replaces the radius. |
Accessibility
- Pagination renders a <nav> named "Pagination", so screen readers list it with the page's landmarks.
- The items are a <ul>, so screen readers say how many there are.
- current sets aria-current="page". The current page also gets an outline, so it is marked by shape, not only by color.
- Previous and Next are named "Previous page" and "Next page". On narrow screens the words are visually hidden but still read. Their chevrons are aria-hidden.
- A disabled link has no href, so it is not a tab stop. It keeps role="link" and aria-disabled="true", so screen readers still say it is there and unavailable.
- PaginationEllipsis has visually hidden text, "More pages" by default. Its dots are aria-hidden, and it is not interactive.
- Links are native <a> elements, so Tab moves between them and Enter follows them.
- Link text uses the tone's soft foreground, held to 4.5:1 on the page. The current page's outline is held to 3:1, and so is the focus ring.
Examples
Page 1 of 10
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious disabled />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=1" current>
1
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2">2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=3">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=10">10</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=2" />
</PaginationItem>
</PaginationContent>
</Pagination>With Next.js links
import Link from "next/link";
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious render={<Link href="?page=4" />} />
</PaginationItem>
<PaginationItem>
<PaginationLink render={<Link href="?page=5" />} current>
5
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext render={<Link href="?page=6" />} />
</PaginationItem>
</PaginationContent>
</Pagination>A row built from the current page
import { Fragment } from "react";
function Pages({ page, total }: { page: number; total: number }) {
const shown = [...new Set([1, page - 1, page, page + 1, total])]
.filter((n) => n >= 1 && n <= total)
.sort((a, b) => a - b);
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href={`?page=${page - 1}`} disabled={page === 1} />
</PaginationItem>
{shown.map((n, i) => (
<Fragment key={n}>
{i > 0 && n - shown[i - 1] > 1 ? (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
) : null}
<PaginationItem>
<PaginationLink href={`?page=${n}`} current={n === page}>
{n}
</PaginationLink>
</PaginationItem>
</Fragment>
))}
<PaginationItem>
<PaginationNext href={`?page=${page + 1}`} disabled={page === total} />
</PaginationItem>
</PaginationContent>
</Pagination>
);
}Above and below a table
<Pagination aria-label="Pages, top">
{/* ... */}
</Pagination>
{/* the table */}
<Pagination aria-label="Pages, bottom">
{/* ... */}
</Pagination>