Skip to content
Component installs need the registry setup
VegaStack Design

Cursor Pager

Previous and Next for keyset paging — a range label and two buttons, no page numbers and no total.

Status
stable
Since
0.25.69
Accessibility pattern
navigation landmark, aria-disabled ends, live range

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 Cursor Pager before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify cursor-pager

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

pnpm dlx shadcn@4.21.0 add @vegastack/cursor-pager

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, @vegastack/use-announcer.

Usage

import { CursorPager } from "@/components/ui/cursor-pager";

const [history, setHistory] = React.useState<(string | null)[]>([]);
const [cursor, setCursor] = React.useState<string | null>(null);

<CursorPager
  label={`Rows ${history.length * 100 + 1}–${history.length * 100 + page.rows.length}`}
  hasPrevious={history.length > 0}
  hasNext={page.nextCursor != null}
  onPrevious={() => {
    setCursor(history.at(-1) ?? null);
    setHistory(history.slice(0, -1));
  }}
  onNext={() => {
    setHistory([...history, cursor]);
    setCursor(page.nextCursor);
  }}
/>;

A keyset-paged list knows whether a page exists on either side and what the current page holds, but not how many pages there are or where page 7 starts. CursorPager shows exactly that: a range label at the leading edge, and Previous and Next. It is controlled and owns no cursors — the host keeps the cursor history (the stack of cursors it came through) and fetches.

DataGrid's pagedMode renders it under the grid, for people who page instead of scroll. For a list with a total and page numbers, reach for DataListPager.

Examples

At the first page, and while a page loads

At either end the button stays in the tab order with aria-disabled; while loading both hold still and the pager is aria-busy.

API Reference

PropTypeDefaultDescription
hasNext*boolean—A page exists after this one.
hasPrevious*boolean—A page exists before this one.
onNext*() => void—Go to the next page (the host pushes the current cursor).
onPrevious*() => void—Go to the previous page (the host pops its cursor history).
labelReact.ReactNode—What the current page holds, shown at the leading edge and announced when it changes — for example "Rows 101–200".
loadingbooleanfalseA page is loading: both buttons hold still until it lands.
nextLabelstring"Next"The Next button's text.
previousLabelstring"Previous"The Previous button's text.

Data attributes and CSS variables on CursorPager

AttributeValues
data-slot"cursor-pager" | "cursor-pager-label" | "cursor-pager-next" | "cursor-pager-previous"

Accessibility

  • A navigation landmark named Pagination (aria-label renames it).
  • Previous and Next are buttons that stay focusable at either end (aria-disabled), rather than disappearing.
  • A new label string is announced politely through one live region; the label on screen at first render is not.
KeyAction
TabMove to Previous, then Next.
Enter / SpaceGo to that page.
ContractStates tested
Behaviourdefault, first-page, last-page, loading
Accessibilitykeyboard, labeled, status-announcement
Visualdefault, disabled

Do / Don't

Do
Keep the cursor history in the host and push the current cursor before following the next one.
Don't
Show a page number or a total you computed by counting pages — a keyset list does not know either.

On this page