AdminPaginatedTable
ImplementedControlled-pagination admin data table — Wave-3 L5-M3 composition of the canonical DataTable organism with the Pagination molecule (first / prev / pages / next / last + page-size readout, hidden on a single page). Pagination is FULLY CONTROLLED: currentPage + onPageChange are required, the wrapper never owns page state, the page is clamped into [1, totalPages] and sliced out of the full rows array. The Pagination molecule hides itself on a single page so short tables don't show dead chrome. Exported from the barrel along with AdminPaginatedTableProps and AdminPaginatedTableColumn.
components/organisms/AdminPaginatedTable/AdminPaginatedTable.tsxControlled-pagination admin data table — a Wave-3 L5-M3 composition of the canonical DataTable organism with the Pagination molecule. The wrapper owns the page slicing while the consumer controls currentPage/onPageChange, mirroring server-driven pages; the Pagination molecule hides itself on a single page.
AdminPaginatedTable
Organism — Admin family
Composes DataTable (table chrome, empty state) with the Pagination molecule (first/prev/pages/next/last + page-size readout, hidden on a single page). The wrapper owns the slicing: pagination is CONTROLLED by the consumer (currentPage + onPageChange), clamped into [1, totalPages]; the table caption gains a 'page X of Y' suffix via ariaLabel.
Controlled pagination — the page lives in this component's state; use first / prev / pages / next / last below (5 rows per page, 23 total).
| Producer | Amount | Paid |
|---|---|---|
| Spectacles XYZ | $250.00 | 2026-08-01 |
| Prod 4SAISON | $287.00 | 2026-08-02 |
| Billetterie MTL | $324.00 | 2026-08-03 |
| Agence Nord | $361.00 | 2026-08-04 |
| Spectacles XYZ | $398.00 | 2026-08-05 |
Current page: 1 of 5
This component supports light and dark mode.
Usage
Pagination is controlled, not owned — keep currentPage in the URL or parent state (shareable admin URLs), pass pageSize matching your API page size, and hand the full rows array over (the wrapper slices — pre-slicing breaks the counter and clamping).
import { useState } from 'react'
import { AdminPaginatedTable } from '@epasslive/design-system'
function PayoutsTable({ rows }: { rows: PayoutRow[] }) {
const [page, setPage] = useState(1)
return (
<AdminPaginatedTable
columns={columns}
rows={rows}
rowKey={(r) => r.id}
pageSize={10}
currentPage={page}
onPageChange={setPage}
ariaLabel="Producer payouts"
/>
)
}Conventions
onPageChange fires only on real moves — out-of-range or same-page requests are swallowed, so no guard is needed in your handler. rowKey indices are absolute (page offset added), so keys stay stable as pages flip. When a single page remains, the Pagination molecule removes itself — no dead chrome on short tables.
Accessibility
Pagination renders as a <nav> landmark with labeled first / previous / next / last controls, numbered page buttons and aria-current on the active page (WCAG 4.1.2). The table caption gains a "page X of Y" suffix, so screen readers announce position within the dataset when the page flips.
Props
columns*AdminPaginatedTableColumn<T>[]Column definitions — see the column table below.
rows*T[]Full row data — the wrapper slices the current page out of it.
pageSize*numberRows per page (floored, minimum 1).
rowKey*(row: T, index: number) => stringStable React key per row — index is the ABSOLUTE pre-slice index.
currentPage*numberCurrent page (1-indexed, controlled). Clamped into [1, totalPages].
onPageChange*(page: number) => voidControlled page-change callback — fired only for a valid, different page.
paginationLabelstring"Table pagination"Accessible label for the pagination nav landmark.
emptyMessagestring"No data to display"Rendered in DataTable's accessible empty state.
ariaLabelstring"Paginated table"Accessible name — the table caption also gains 'page X of Y'.
pageAnnouncementstring"Page {page} of {total}"Template for the sr-only aria-live announcement when the page changes — tokens {page} and {total} are substituted. EN default — FR: 'Page {page} sur {total}'.
classNamestringClass on the outer section wrapper.
idstringStable id (auto-generated apg-<useId()> fallback).
Column definition
id*stringColumn identifier.
header*stringHeader label.
cell*(row: T) => React.ReactNodeCell renderer — receives the row object.
widthstringCSS width for the column.
align"left" | "right" | "center"Cell + header alignment.
Props
AdminPaginatedTable
columnsAdminPaginatedTableColumn<T>[]—Column definitions — id / header / cell / width / align. See AdminPaginatedTableColumn below.
rowsT[]—FULL row data — the wrapper slices the current page out of it. Do not pre-slice (breaks the counter and clamping).
pageSizenumber—Rows per page — floored, minimum 1.
rowKey(row: T, index: number) => string—Stable React key per row — index is the ABSOLUTE pre-slice index, so keys stay stable as pages flip.
currentPagenumber—Current page (1-indexed, CONTROLLED). Clamped into [1, totalPages].
onPageChange(page: number) => void—Controlled page-change callback — fires ONLY for a valid, different page (out-of-range / same-page requests are swallowed).
paginationLabelstring'Table pagination'Accessible label for the pagination <nav> landmark. EN default — FR: 'Pagination de la table'.
emptyMessagestring'No data to display'Empty-state cell copy (DataTable's role=status / aria-live=polite). EN default — FR: 'Aucune donnée à afficher'.
ariaLabelstring'Paginated table'Accessible name for the section and the table caption — the caption also gains 'page X of Y'. EN default — FR: 'Table paginée'.
pageAnnouncementstring'Page {page} of {total}'Template for the sr-only aria-live announcement when the page changes. Tokens: {page}, {total}. EN default — FR: 'Page {page} sur {total}'.
classNamestring—Class on the outer section wrapper.
idstringapg-<useId()>Stable id for the wrapper section (auto-generated apg-<useId()> fallback).
<AdminPaginatedTable />Accessibility
ARIA
2 rulescontrol
1 ruleUsage
Import
import { AdminPaginatedTable } from "@epasslive/design-system"Example 1
<AdminPaginatedTable columns={cols} rows={rows} rowKey={(r) => r.id} pageSize={10} currentPage={page} onPageChange={setPage} ariaLabel="Producer payouts" />Example 2
<AdminPaginatedTable columns={cols} rows={commandes} rowKey={(r) => r.id} pageSize={10} currentPage={page} onPageChange={setPage} ariaLabel="Paiements producteurs" paginationLabel="Pagination des paiements" pageAnnouncement="Page {page} sur {total}" emptyMessage="Aucune donnée à afficher" />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not pre-slice rows before passing them — the wrapper slices (pre-slicing breaks the counter and the [1, totalPages] clamping)
— Do not use this organism for client-side pagination of a small dataset that fits in memory — use AdminDataTable without a wrapper; pagination has UX cost (numbered pages, first/last)
— Do not try to control the page via local useState AND skip the onPageChange wiring — currentPage is the source of truth; pass it through
— Do not reach for this organism when you also need sorting or selection — those are canonical DataTable props the wrapper does not expose; compose DataTable + Pagination (or ListPanel) directly