Pagination
Page navigation — previous and next controls, numbered page links, an ellipsis for long ranges, and the active page.
- Status
- Since
0.1.0- Accessibility pattern
- navigation landmark
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Pagination before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify paginationInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/paginationRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button.
Usage
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
<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>;Anatomy
Examples
Composition
Pagination is a nav labelled pagination; PaginationContent is the ul inside it, and every
control sits in its own PaginationItem. PaginationLink renders a plain <a>
styled with buttonVariants (outline when isActive, ghost otherwise), so anchor props —
href, onClick — go straight on PaginationLink.
Pagination
└── PaginationContent
├── PaginationItem
│ └── PaginationPrevious
├── PaginationItem
│ └── PaginationLink
├── PaginationItem
│ └── PaginationEllipsis
└── PaginationItem
└── PaginationNextSimple
Page numbers with no previous/next pair. isActive marks the current page: it swaps the link to the
outline button variant and sets aria-current="page".
Icons Only
The previous/next pair on its own, next to a rows-per-page select — the footer a data table wants.
PaginationPrevious and PaginationNext hide their label below the sm breakpoint, so on a phone
the two controls are the chevrons alone while aria-label keeps their names.
Next.js
PaginationLink renders a native anchor and accepts native anchor props. It does not accept
Next-only prefetch or scroll. For client routing, compose next/link inside PaginationItem
and apply the same buttonVariants recipe; mark the current page with aria-current="page".
Keep PaginationLink for ordinary href navigation, as in this plain-anchor example.
RTL
Wrap the subtree in DirectionProvider direction="rtl". The chevrons carry rtl:rotate-180, so
they point the way the text reads, and text on PaginationPrevious/PaginationNext is what makes
the two labels translatable — they default to Previous and Next.
Playground
Move the page-link size across the shared control scale, then copy the generated JSX.
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="?page=1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=1">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=2" isActive>2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="?page=3">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="?page=3" />
</PaginationItem>
</PaginationContent>
</Pagination>API Reference
Pagination
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — |
PaginationContent
PaginationContent adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
PaginationItem
PaginationItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
PaginationLink
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | — | |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — |
PaginationPrevious
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | — | |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — | |
text | string | — |
PaginationNext
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | — | |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — | |
text | string | — |
PaginationEllipsis
| Prop | Type | Default | Description |
|---|---|---|---|
morePagesLabel | string | — |
Accessibility
Paginationis anavwithrole="navigation"named "Pagination" (labelrenames it), so assistive technology can jump to the pager as a landmark and name it apart from the page's other navigation.- The active page carries
aria-current="page", which is what announces "current" rather than theoutlinefill doing it with colour alone. - Every control is a real
<a href>wearing theButtonrecipe, so it announces as a link, activates on Enter, and navigates and opens in a new tab like the anchor it is (A11Y-23). Upstream rendered it throughButtonwithnativeButton={false}, which stampedrole="button"on a link. For a pager whose controls change state instead of navigating, use buttons —DataListPagerdoes. PaginationPreviousandPaginationNextkeeparia-label="Go to previous page"andaria-label="Go to next page", so they stay named at the breakpoint where their visible label is hidden.PaginationEllipsisisaria-hiddendecoration carrying ansr-only"More pages" (morePagesLabelrenames it); it is not a control and is not in the tab order.- Every control is a
Button—size="icon"(32px) for a page number,size="default"for the previous/next pair — so the WCAG 2.2 target floor is met by the control's own box, with no invisible hit area needed (A11Y-2). - Focus is the global 2px
:focus-visibleoutline frombase.css, inherited fromButton; there is no ring glow.
| Contract | States tested |
|---|---|
| Behaviour | default, active, hover, pressed |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, current, labeled, presentation-only-decoration |
| Visual | default, active, hover, focus-visible |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/pagination.patch. Every hunk:
- A11Y-23 —
PaginationLinkrenders a real<a>wearingbuttonVariants({ variant, size })instead ofButton nativeButton={false} render={<a/>}, so a pagination link keeps the link role (upstream's version announced a button);aria-current="page"stays. - VOI-1 — upstream's lower-case
aria-label="pagination"becomes "Pagination", overridable withlabel, and the ellipsis's "More pages" is overridable withmorePagesLabel. - No styling hunk.
PaginationLinkwears theButtonrecipe, which already carries this system's focus, cursor and disabled behaviour, so pagination inherits FOC-1, FOC-6, FOC-14 and FRM-4 rather than restating them. A11Y-2 — audited, no hunk: every control is aButtonatsize="icon"(32px) orsize="default", andPaginationEllipsisisaria-hiddenand not a control.pagination.test.tsxmeasures both. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.