Skip to content
Component installs need the registry setup
VegaStack Design

Data List Pager

A controlled paging footer for DataList — a range summary, a rows-per-page chooser, and windowed page controls.

Status
stable
Since
0.13.0
Accessibility pattern
navigation landmark, labelled page-size select, live range

Last updated

InvoiceCustomerAmount
INV-1001Acme$120
INV-1002Globex$499
INV-1003Initech$878
INV-1004Umbrella$1,257
INV-1005Hooli$1,636
INV-1006Stark$2,015
INV-1007Acme$2,394
INV-1008Globex$373
INV-1009Initech$752
INV-1010Umbrella$1,131
INV-1011Hooli$1,510
INV-1012Stark$1,889
INV-1013Acme$2,268
INV-1014Globex$247
INV-1015Initech$626

1–15 of 40

Rows per page

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

pnpm exec vegastack-design verify data-list-pager

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

pnpm dlx shadcn@4.21.0 add @vegastack/data-list-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/data-table-parts, @vegastack/pagination, @vegastack/select, @vegastack/use-announcer.

Usage

import { DataList } from "@/components/ui/data-list";
import { DataListPager } from "@/components/ui/data-list-pager";

const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(15);

<DataList
  columns={columns}
  data={rows.slice((page - 1) * pageSize, page * pageSize)}
  getRowId={(row) => row.id}
  footer={
    <DataListPager
      page={page}
      pageSize={pageSize}
      total={rows.length}
      onPageChange={setPage}
      onPageSizeChange={(size) => {
        setPageSize(size);
        setPage(1);
      }}
    />
  }
/>;

DataListPager is built for DataList's footer slot. It composes Pagination and Select and adds no visual primitive of its own: a range summary in tabular numerals ("1–15 of 40"), a rows-per-page chooser, and a windowed page list with Previous and Next.

It is controlled and owns no data. The host holds page (1-based) and pageSize, slices or fetches the rows, and passes total. A server-paged host sends limit = pageSize and offset = (page - 1) * pageSize. Changing the page size only reports the request — whether that returns to page 1 (as above) or keeps the first visible row is the host's policy.

InvoiceCustomerAmount
INV-1001Acme$120
INV-1002Globex$499
INV-1003Initech$878
INV-1004Umbrella$1,257
INV-1005Hooli$1,636
INV-1006Stark$2,015
INV-1007Acme$2,394
INV-1008Globex$373
INV-1009Initech$752
INV-1010Umbrella$1,131
INV-1011Hooli$1,510
INV-1012Stark$1,889
INV-1013Acme$2,268
INV-1014Globex$247
INV-1015Initech$626

1–15 of 40

Rows per page

Examples

Many pages

With more than seven pages the list collapses into a window: the first and last page, the current page and its neighbours, and an ellipsis for each collapsed run.

76–90 of 240

Rows per page

A single page

When every row fits on one page the page controls are hidden. The range and the rows-per-page chooser stay, so a reader can still widen or narrow the page. An empty list reads "0 of 0".

1–9 of 9

Rows per page

Narrow containers

The page list follows the pager's own width, not the viewport's, and never wraps — a wrapped run of page numbers reads as two lists. It changes layout instead (data-layout on the root), while the range and the rows-per-page chooser wrap onto their own lines above it:

data-layoutPager widthPage list
full480px and upLabelled Previous / Next, the current page and one neighbour each side
compact240–479pxIcon-only ends, the current page with no neighbours
minimalbelow 240pxIcon-only ends around Page 3 of 12, or 3 / 12 when that is too long

Width picks the starting layout for page numbers of up to two digits. A number slot is at least 32px and grows to hold its number, so a longer page count can outgrow that layout (a three-digit last page already makes the compact list about 241px); the pager then measures every page item against its own box, with no tolerance, and steps down one layout at a time until the page list fits, before first paint. The check is re-taken on any change to the pager's width, however small, and whenever an element of the page list changes size on its own — a web font swapping in, say — so a late font cannot push Next outside. At the last step the position shortens to N / M (data-short on data-slot="data-list-pager-position") and truncates if it still has no room, while assistive technology always hears Page N of M. So the page list never scrolls sideways at any page count. The range and the rows-per-page chooser are single lines that do not break; at a seven-digit total they need about 200px, which is the pager's practical floor. The server render is minimal, and the client corrects it before first paint.

Page sizes

pageSizes sets the choices (default [15, 30, 50]). A pageSize that is not in the list is added to it, so the chooser always shows the current value.

Numbers that are not numbers render a sane state rather than NaN: a total that is NaN, undefined (a count still loading) or negative reads as 0 ("0 of 0", no page controls); a page that is not finite reads as 1; and a pageSize that is not a positive number — 0 included — shows the first pageSizes entry and is never added to the chooser. pageSizeLabel renames the chooser — it is both the visible label and the accessible name.

<DataListPager
  page={page}
  pageSize={pageSize}
  total={total}
  pageSizes={[10, 25, 100]}
  pageSizeLabel="Invoices per page"
  onPageChange={setPage}
  onPageSizeChange={setPageSize}
/>

Without page size

Omit onPageSizeChange for a fixed page size: the pager shows the range and the page controls, and no rows-per-page chooser.

21–40 of 87

API Reference

PropTypeDefaultDescription
onPageChange*(page: number) => void—Called with the next 1-based page when a page control is activated.
page*number—The active page, 1-based. Clamped into range for display; a non-finite value (NaN, undefined from a loading query) reads as page 1.
pageSize*number—Rows per page. A value that is not a positive finite number (0, a negative, NaN, undefined) is not a page size: the pager shows the first pageSizes entry instead, and never adds the bad value to the chooser.
total*number—Total number of rows across every page. A value that is not a finite number ≥ 0 (NaN, undefined while a count loads, a negative) reads as 0, so the pager renders its empty state ("0 of 0") rather than NaN.
onPageSizeChange((pageSize: number) => void)—Called with the next page size when the rows-per-page choice changes. The page is left alone — reset it in this handler if that is your policy. Omit it for a fixed page size: the rows-per-page chooser is not rendered.
pageSizeLabelstring"Rows per page"Visible label and accessible name of the rows-per-page chooser.
pageSizesreadonly number[][15, 30, 50]The rows-per-page choices. A valid pageSize that is not in the list is added, so the chooser always shows the current value. Entries that are not positive finite numbers are dropped; an empty result falls back to the default list.

Data attributes and CSS variables on DataListPager

AttributeValues
data-activemirrors a prop or state value
data-layoutmirrors a prop or state value
data-short""
data-slot"data-list-pager" | "data-list-pager-nav" | "data-list-pager-next" | "data-list-pager-page" | "data-list-pager-position" | "data-list-pager-previous" | "data-list-pager-range" | "data-list-pager-size"

pagerWindow(current, pages) is exported too: it returns the windowed page list the pager renders (number | "ellipsis" entries), for a host that draws its own page controls.

Accessibility

  • The page list is a navigation landmark named Pagination. Its controls are buttons — they change the page in place rather than navigating — each page named Go to page N, with the current page marked aria-current="page".
  • Previous and Next stay in the tab order at either end. They carry aria-disabled="true" and do nothing when activated, rather than disappearing or dropping pointer events.
  • The rows-per-page Select is named by its visible label (aria-labelledby), so the name a screen reader speaks is the text on screen.
  • A page change announces the new range politely (Showing 16–30 of 40) through one live region mounted for the pager's life; the range on screen at first render is not announced.
KeyAction
TabMove through the rows-per-page chooser and the page controls.
Enter / SpaceActivate the focused page control, or open the chooser.
↑ / ↓Move through the page sizes while the chooser is open.
EscapeClose the chooser without changing the page size.
ContractStates tested
Behaviourdefault, first-page, last-page, single-page, empty, out-of-range, open
Accessibilitylabeled, current, disabled, live, status-announcement, browser-accessibility-test
Visualdefault, active, hover, focus-visible, disabled

Do / Don't

Do
Keep page and pageSize in the host (or the URL) and pass DataList only the current page's rows.
Don't
Pass DataList every row and expect the pager to slice them — the pager owns no data.

On this page