AdminSortableTable
ImplementedSortable admin data table — Wave-3 L5-M3 thin composition over the canonical DataTable organism that keeps the historical Admin column API (accessor / cell, sortable-by-default columns, numeric data anchors, hideBelow responsive pruning). DataTable owns the three-state sort cycle (none → asc → desc → none), aria-sort semantics, keyboard-accessible header buttons (Tab / Enter / Space), row hover and the accessible empty state (role=status / aria-live=polite). Columns are sortable BY DEFAULT (sortable: false opts out — the inverse of DataTable's opt-in convention). Re-exports SortDirection for legacy barrel consumers. Exported from the barrel along with AdminSortableTableProps and AdminSortableTableColumn.
components/organisms/AdminSortableTable/AdminSortableTable.tsxSortable admin data table — a thin composition over the canonical DataTable organism that keeps the historical Admin column API (accessor/cell, sortable-by-default columns, numeric data anchors, hideBelow responsive pruning) while DataTable owns the sort cycle, aria-sort semantics, keyboard-accessible header buttons and the accessible empty state.
AdminSortableTable
Organism — Admin family
Wave-3 L5-M3 composition over the canonical DataTable organism. The historical Admin column API is preserved: accessor(row) feeds the sort comparator (Dates compare as epoch millis), cell(row) renders the display value, columns are sortable BY DEFAULT (sortable: false opts out — the inverse of DataTable’s opt-in convention), numeric marks data-anchor columns (PRIMARY slot + tabular-nums, ADR-030 A2), and onRowClick makes rows interactive (role="button", Enter/Space).
Click a header to re-sort (Enter / Space works too — each sortable header is a button). Rows are interactive: activate one to select it.
| Yelle en spectacle | 640 | $35,200.00 | 2026-09-02 |
| Soirée Jazz | 312 | $12,012.00 | 2026-09-01 |
| Souper de Noël | 96 | $9,120.00 | 2026-08-30 |
| Nuit du Piano | 203 | $6,090.00 | 2026-09-02 |
| Bal des Boys | 148 | $4,440.00 | 2026-08-28 |
Aucune ligne sélectionnée — activez-en une avec Entrée ou Espace.
This component supports light and dark mode.
Usage
AdminSortableTable is generic over its row type — define columns with accessor (feeds the sort comparator; Dates compare as epoch millis) and optional cell (display formatting), pass rows and a stable rowKey, and the component owns the sort cycle from there. Columns are sortable by default; opt filter-dimension columns out with sortable={false}. Wire onRowClick to make rows interactive — the preview above keeps the selection in a polite live region.
import { AdminSortableTable } from '@epasslive/design-system'
const columns: AdminSortableTableColumn<SalesRow>[] = [
{ id: 'event', header: 'Event', accessor: (r) => r.event },
{ id: 'channel', header: 'Channel', accessor: (r) => r.channel, sortable: false, hideBelow: 'sm' },
{ id: 'qty', header: 'Tickets', accessor: (r) => r.qty, numeric: true },
{ id: 'gross', header: 'Gross', accessor: (r) => r.gross, numeric: true, cell: (r) => fmtCad(r.gross) },
{ id: 'soldAt', header: 'Last sale', accessor: (r) => r.soldAt, numeric: true,
cell: (r) => r.soldAt.toLocaleDateString('en-CA') },
]
<AdminSortableTable
columns={columns}
rows={rows}
rowKey={(r) => r.id}
defaultSort={{ columnId: 'gross', direction: 'desc' }}
ariaLabel={t('salesByEvent')}
onRowClick={setSelected}
/>Column conventions
Keep accessor primitive (string / number / Date) — it feeds the comparator; format in cell, which receives the original row object. Mark figure columns numeric so they render in Inter with tabular-nums and align down the column (ADR-030 A2). Use hideBelow for secondary columns that should prune on narrow viewports. When you need selection checkboxes, row grouping or rowHref navigation, compose the canonical DataTable directly — the Admin wrapper intentionally does not expose those props.
Accessibility
Every sortable header is a real <button> — Tab reaches it and Enter / Space cycles none → ascending → descending → none, with aria-sort declared on the sorting header and omitted on non-sortable ones (WCAG 1.3.1). With onRowClick, rows expose role="button" and activate on Enter / Space. Pass ariaLabel so the table is named (WCAG 4.1.2) — it also feeds the table caption. The empty state renders in a role="status" / aria-live="polite" cell and is announced without stealing focus.
Props
columns*AdminSortableTableColumn<T>[]Column definitions — see the column table below.
rows*T[]Row data — one item per row, typed by the generic.
rowKey*(row: T, i: number) => stringStable React key per row (i is the pre-sort index).
defaultSort{ columnId: string; direction: "asc" | "desc" }Initial sort — uncontrolled afterwards (sorting is component-owned state).
emptyMessagestring"Aucune donnée."Rendered in the empty state cell (role="status", aria-live="polite"). EN consumers: pass "No data to display".
ariaLabelstring"Tableau de données triable"Accessible name — also feeds the table caption. EN consumers: pass "Sortable data table".
sortLabel(header: string, direction: 'asc' | 'desc' | null) => string(h) => `Trier par ${h}`Accessible-name template for sort toggle buttons. Receives the column header and the current sort direction. EN consumers: pass `(h) => \`Sort by ${h}\``.
classNamestringClass on the rendered table wrapper.
idstringStable id (auto-generated asort-<useId()> fallback).
onRowClick(row: T) => voidMakes rows interactive (role="button", Enter / Space activation) — receives the original row object.
Column definition
AdminSortableTableColumn<T> — the historical Admin column API, preserved verbatim by the Wave-3 composition.
id*stringColumn identifier — feeds defaultSort.columnId and the sort state.
header*stringHeader label.
accessor*(row: T) => string | number | Date | nullValue fed to the sort comparator (Dates are normalized to epoch millis) and the display fallback.
cell(row: T) => React.ReactNodeCustom cell renderer — receives the ORIGINAL row object (e.g. currency/date formatting).
widthstringCSS width for the column.
align"left" | "right" | "center"Cell + header alignment.
sortablebooleantrueSortable BY DEFAULT — pass false to opt out (filter-dimension columns). Inverse of DataTable's opt-in convention.
hideBelow"sm" | "md" | "lg"Prunes the column below the given viewport width.
numericbooleanData-anchor columns (prices, quantities, dates as figures): PRIMARY slot (Inter) + tabular-nums (ADR-030 A2) instead of SECONDARY (Space Grotesk).
Props
AdminSortableTable
columnsAdminSortableTableColumn<T>[]—Column definitions — id / header / accessor (required) / cell / width / align / sortable / hideBelow / numeric. See AdminSortableTableColumn below.
rowsT[]—Row data — one item per row, typed by the generic.
rowKey(row: T, i: number) => string—Stable React key per row — i is the PRE-SORT index.
defaultSort{ columnId: string; direction: 'asc' | 'desc' }—Initial sort — uncontrolled afterwards (sorting is component-owned state). After mount the cycle is component-owned; a controlled onSort callback is not yet exposed (future DataTable enhancement).
emptyMessagestring'Aucune donnée.'Empty-state cell copy (DataTable's role=status / aria-live=polite). FR default — EN consumers pass 'No data to display'.
ariaLabelstring'Tableau de données triable'Accessible name for the table region + caption. FR default — EN consumers pass 'Sortable data table'.
sortLabel(header: string, direction: 'asc' | 'desc' | null) => string(h) => `Trier par ${h}`Accessible-name template for sort toggle buttons — receives the column header and the current direction. FR default — EN consumers pass `(h) => `Sort by ${h}``.
classNamestring—Class on the rendered DataTable wrapper.
idstringasort-<useId()>Stable id for the scroll wrapper (auto-generated asort-<useId()> fallback).
onRowClick(row: T) => void—Makes rows interactive — focusable <tr> (tabIndex=0, Enter/Space activation). The <tr> KEEPS its row role (DataTable never swaps it for role='button') so cell semantics survive.
captionstringariaLabelTable caption. Defaults to ariaLabel when omitted — pass explicitly when the caption should read differently from the region's accessible name.
density'comfortable' | 'compact''comfortable'Row density — compact switches the per-cell padding to a tighter rhythm via DataTable's density prop.
stripedbooleanfalseZebra-striping — tint every odd body row with the secondary surface token (theme-swapping, AA both themes).
<AdminSortableTable />Accessibility
ARIA
3 rulesUsage
Import
import { AdminSortableTable } from "@epasslive/design-system"Example 1
<AdminSortableTable columns={cols} rows={rows} rowKey={(r) => r.id} />Example 2
<AdminSortableTable columns={cols} rows={sales} rowKey={(r) => r.id} defaultSort={{ columnId: "gross", direction: "desc" }} ariaLabel="Ticket sales by event" onRowClick={setSelected} />Example 3
<AdminSortableTable columns={cols} rows={ventes} rowKey={(r) => r.id} ariaLabel="Ventes de billets par événement" emptyMessage="Aucune donnée." sortLabel={(h) => `Trier par ${h}`} onRowClick={setSelected} />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not format values inside accessor — return the primitive (string / number / Date); format in cell which receives the original row object (the accessor value is the display fallback)
— Do not mark filter-dimension columns (channel, status) sortable — they mislead users (use sortable: false to opt out, the inverse of DataTable)
— Do not try to control the sort cycle from outside — sorting is component-owned state after defaultSort; a controlled onSort is not yet exposed (future DataTable enhancement)
— Do not reach for this organism when you need bulk selection (use AdminBulkActionTable), search filtering (use AdminFilterableTable) or pagination (use AdminPaginatedTable) — those are sibling compositions over the same DataTable