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

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

pnpm dlx shadcn@latest add @vegastack/data-list-pager

The same command installs the registry items it composes: @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}
/>

API Reference

PropTypeDefaultDescription
onPageChange*(page: number) => voidCalled with the next 1-based page when a page control is activated.
onPageSizeChange*(pageSize: number) => voidCalled 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.
page*numberThe active page, 1-based. Clamped into range for display; a non-finite value (NaN, undefined from a loading query) reads as page 1.
pageSize*numberRows 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*numberTotal 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.
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-layoutmirrors a prop or state value
data-short""
data-slot"data-list-pager" | "data-list-pager-nav" | "data-list-pager-position" | "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 controls are upstream's Pagination: a navigation landmark named pagination, each page link named Go to page N, and 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