Skip to content
Component installs need the registry setup
VegaStack Design

Pagination

Page navigation — previous and next controls, numbered page links, an ellipsis for long ranges, and the active page.

Status
stable
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 pagination

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/pagination

Run 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

Pagination — data-slot="pagination"
PaginationContent — data-slot="pagination-content"
PaginationEllipsis — data-slot="pagination-ellipsis"
PaginationItem — data-slot="pagination-item"
PaginationLink — data-slot="pagination-link"
PaginationNext
PaginationPrevious

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
        └── PaginationNext

Simple

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

PropTypeDefaultDescription
labelstring—

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).

PropTypeDefaultDescription
isActiveboolean—
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null—

PaginationPrevious

PropTypeDefaultDescription
isActiveboolean—
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null—
textstring—

PaginationNext

PropTypeDefaultDescription
isActiveboolean—
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null—
textstring—

PaginationEllipsis

PropTypeDefaultDescription
morePagesLabelstring—

Accessibility

  • Pagination is a nav with role="navigation" named "Pagination" (label renames 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 the outline fill doing it with colour alone.
  • Every control is a real <a href> wearing the Button recipe, 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 through Button with nativeButton={false}, which stamped role="button" on a link. For a pager whose controls change state instead of navigating, use buttons — DataListPager does.
  • PaginationPrevious and PaginationNext keep aria-label="Go to previous page" and aria-label="Go to next page", so they stay named at the breakpoint where their visible label is hidden.
  • PaginationEllipsis is aria-hidden decoration carrying an sr-only "More pages" (morePagesLabel renames 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-visible outline from base.css, inherited from Button; there is no ring glow.
ContractStates tested
Behaviourdefault, active, hover, pressed
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, current, labeled, presentation-only-decoration
Visualdefault, active, hover, focus-visible

Do / Don't

Do
Use Pagination for content split into numbered pages, mark the current page with isActive, and give every control a real href so it works without JavaScript.
Don't
Use Pagination for an infinite feed or a 'load more' list, or rely on the ellipsis as a control — it is aria-hidden decoration.

Deviations

Upstream's file plus packages/ui/upstream/patches/pagination.patch. Every hunk:

  • A11Y-23 — PaginationLink renders a real <a> wearing buttonVariants({ variant, size }) instead of Button 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 with label, and the ellipsis's "More pages" is overridable with morePagesLabel.
  • No styling hunk. PaginationLink wears the Button recipe, 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 a Button at size="icon" (32px) or size="default", and PaginationEllipsis is aria-hidden and not a control. pagination.test.tsx measures both.
  • DOC-2 — import { cn } from "cn" becomes @vegastack/design; our npm layer owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header the three-copy model needs; no decision of its own.

On this page