Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Pagination

Page navigation — previous/next, 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

Add Pagination from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/pagination

The same command installs the registry items it composes: @vegastack/icon-button.

Usage

import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination";

<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>
      <PaginationEllipsis />
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="?page=2" />
    </PaginationItem>
  </PaginationContent>
</Pagination>;

Anatomy

Pagination is a compound component built from a presentational <nav> wrapping an unordered list of page controls. Compose the parts inside the root — every part exposes a data-slot:

Pagination — data-slot="pagination"
PaginationContent — data-slot="pagination-content"
PaginationEllipsis — data-slot="pagination-ellipsis"
PaginationItem — data-slot="pagination-item"
PaginationLink
PaginationNext
PaginationPager — data-slot="pagination-pager" | "pagination-pager-next" | "pagination-pager-previous"
PaginationPrevious
<Pagination aria-label="Search results pagination">
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href="?page=1" />
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=1">1</PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationLink href="?page=2" isActive>
        2 {/* current page */}
      </PaginationLink>
    </PaginationItem>
    <PaginationItem>
      <PaginationEllipsis /> {/* collapse a long range */}
    </PaginationItem>
    <PaginationItem>
      <PaginationNext href="?page=2" />
    </PaginationItem>
  </PaginationContent>
</Pagination>
  • Pagination — the navigation landmark (data-slot="pagination"). Renders a plain <nav><nav> is the landmark, so no explicit role — labelled "Pagination" unless you pass your own aria-label.
  • PaginationContent — the list of controls (<ul>, data-slot="pagination-content"). Flex-aligned with a consistent gap.
  • PaginationItem — a single list slot (<li>, data-slot="pagination-item").
  • PaginationLink — a navigable page link (<a>, data-slot="pagination-link"). Styled like a ghost/outline button; set isActive on the current page and choose a size. Supports the Base UI render prop for client-side routing.
  • PaginationPrevious — a labelled "previous page" control (<a>) with a leading chevron.
  • PaginationNext — a labelled "next page" control (<a>) with a trailing chevron.
  • PaginationEllipsis — a decorative collapsed-pages indicator (data-slot="pagination-ellipsis", aria-hidden) for long ranges.

Examples

Use the render prop on PaginationLink (and PaginationPrevious/PaginationNext) to compose with your router's link component while keeping pagination styling and semantics:

import Link from "next/link";

<PaginationLink render={<Link href="?page=2" />} isActive>
  2
</PaginationLink>;

Sizes

PaginationLink takes a size prop — sm (28px), md (32px), lg (40px), and icon (a 32px square, the square default for numbered pages) — on the same 28/32/40 control scale as buttons and inputs. The grid below renders all four: icon is the squared-off default, sm/md/lg widen the hit target with horizontal padding.

sm
md
lg
icon

Edge states

When a previous/next control is unavailable, omit href and set aria-disabled="true" — the link then enforces the disabled state itself (removed from the tab order, clicks and Enter blocked). A disabled link can never navigate.

On the first page, disable PaginationPrevious:

On the last page, mirror the pattern on PaginationNext:

Positional pager

PaginationPager is the compact record-pager: previous/next steppers + a "n of N [context]" status label (bounds-disabled at the ends, tabular numerals). Use it for stepping through items of a known list; keep Pagination for numbered page navigation.

3 of 10 in All Companies

Playground

Try every page-link size on a live pagination bar, 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

PropTypeDefaultDescription
isActivebooleanfalseMarks the link as the current page — applies the active **primary**-fill styling and sets aria-current="page".
renderuseRender.RenderProp<Record<string, unknown>>Replace the rendered <a> element via Base UI render composition. Pass a routing link element (e.g. <NextLink href="/x" />) or a render function to integrate with a router while keeping pagination styling.
size"icon" | "lg" | "md" | "sm"

PaginationPrevious / PaginationNext

PaginationPrevious and PaginationNext accept exactly the same props as PaginationLink (isActive, size, render, plus native <a> props) — they pre-compose the directional chevron and label.

Pagination, PaginationContent, PaginationItem, and PaginationEllipsis add no props of their own — each accepts everything its underlying native element accepts (<nav>, <ul>, <li>, <span>), plus className and ref.

Accessibility

  • The root is a navigation landmark: a plain <nav aria-label="Pagination">. <nav> already carries the landmark role, so the component does not restate it.
  • Name every pager on a page that has more than one. Two landmarks with the same accessible name are an axe landmark-unique failure and give screen-reader users no way to tell them apart. Pass aria-label="Search results pagination", aria-label="Invoices pagination", and so on — the default "Pagination" is correct only when the page has exactly one.
  • The active page uses aria-current="page", so it is announced as the current location in the set.
  • Previous/Next controls carry an explicit aria-label ("Go to previous page" / "Go to next page") so the icon-only direction is still meaningful to screen readers.
  • PaginationEllipsis is decorative (aria-hidden="true"). Use real page links or a separate labelled menu trigger when skipped pages need to be reachable.
  • Disabled Previous/Next controls omit href and set aria-disabled — the component then removes them from the tab order and blocks activation itself (an aria-disabled anchor is otherwise not inert).
  • Links use the native <a> and keep a visible :focus-visible ring — never outline: none.
KeyAction
TabMove focus through the pagination controls.
EnterFollow the focused page link.
ContractStates tested
Behaviourdefault, active, collapsed, disabled
Accessibilitycurrent, disabled, labeled, status-announcement, semantic-html
Visualdefault, hover, active, disabled

Do / Don't

Do
Mark the current page with isActive so it gets aria-current='page', give icon-only Previous/Next controls a clear label, and name each pager when a page has more than one.
Don't
Render the active page as a plain non-link, omit Previous/Next labels, or leave two pagers sharing the default 'Pagination' name.

On this page