Data List Pager
A controlled paging footer for DataList — a range summary, a rows-per-page chooser, and windowed page controls.
- Status
- Since
0.13.0- Accessibility pattern
- navigation landmark, labelled page-size select, live range
Last updated
| Invoice | Customer | Amount |
|---|---|---|
| INV-1001 | Acme | $120 |
| INV-1002 | Globex | $499 |
| INV-1003 | Initech | $878 |
| INV-1004 | Umbrella | $1,257 |
| INV-1005 | Hooli | $1,636 |
| INV-1006 | Stark | $2,015 |
| INV-1007 | Acme | $2,394 |
| INV-1008 | Globex | $373 |
| INV-1009 | Initech | $752 |
| INV-1010 | Umbrella | $1,131 |
| INV-1011 | Hooli | $1,510 |
| INV-1012 | Stark | $1,889 |
| INV-1013 | Acme | $2,268 |
| INV-1014 | Globex | $247 |
| INV-1015 | Initech | $626 |
1–15 of 40
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-pagerThe 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.
| Invoice | Customer | Amount |
|---|---|---|
| INV-1001 | Acme | $120 |
| INV-1002 | Globex | $499 |
| INV-1003 | Initech | $878 |
| INV-1004 | Umbrella | $1,257 |
| INV-1005 | Hooli | $1,636 |
| INV-1006 | Stark | $2,015 |
| INV-1007 | Acme | $2,394 |
| INV-1008 | Globex | $373 |
| INV-1009 | Initech | $752 |
| INV-1010 | Umbrella | $1,131 |
| INV-1011 | Hooli | $1,510 |
| INV-1012 | Stark | $1,889 |
| INV-1013 | Acme | $2,268 |
| INV-1014 | Globex | $247 |
| INV-1015 | Initech | $626 |
1–15 of 40
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
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
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-layout | Pager width | Page list |
|---|---|---|
full | 480px and up | Labelled Previous / Next, the current page and one neighbour each side |
compact | 240–479px | Icon-only ends, the current page with no neighbours |
minimal | below 240px | Icon-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
| Prop | Type | Default | Description |
|---|---|---|---|
onPageChange* | (page: number) => void | — | Called with the next 1-based page when a page control is activated. |
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. |
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. |
pageSizeLabel | string | "Rows per page" | Visible label and accessible name of the rows-per-page chooser. |
pageSizes | readonly 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
| Attribute | Values |
|---|---|
data-layout | mirrors 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: anavigationlandmark named pagination, each page link named Go to page N, and the current page markedaria-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
Selectis 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.
| Key | Action |
|---|---|
| Tab | Move through the rows-per-page chooser and the page controls. |
| Enter / Space | Activate the focused page control, or open the chooser. |
| ↑ / ↓ | Move through the page sizes while the chooser is open. |
| Escape | Close the chooser without changing the page size. |
| Contract | States tested |
|---|---|
| Behaviour | default, first-page, last-page, single-page, empty, out-of-range, open |
| Accessibility | labeled, current, disabled, live, status-announcement, browser-accessibility-test |
| Visual | default, active, hover, focus-visible, disabled |