Skip to content

Components · Tables and data grid

DataTable

Column-driven data table with sorting, selection, column controls, pagination, states and keyboard grid navigation; plus the Table parts it is built on.

@rookcom/monarch-react/dataComponent

Sorting, selection, row actions and grid keyboard

Invoices
Actions
LumenDue2026-09-24Ben$1,720
Westbridge ImportsPaid2026-09-20Dev$2,150
Tidewater ToysDue2026-09-18Ana$395
Cobalt LtdDue2026-09-11Ben$860
Northfield TradersPaid2026-09-02Ana$1,240
Fernway IncOverdue2026-08-21Chen$4,300

1 selected

Usage examples

Server-driven sorting and pagination

Column chooser, resizing and expandable rows

Loading, filtered empty and error states

Card layout for narrow screens

Table parts (custom tables)

Purpose

Shows records in rows and columns that people compare, sort, select and act on. DataTable takes column definitions and rows and handles sorting, selection, column visibility, order and width, pagination, row actions, expandable rows, loading, empty and error states, keyboard navigation and a card layout for phones. The Table* parts are the styled native table it is built on, for fully custom tables.

Use when

  • Tabular records the user compares across columns (tickets, invoices, members).
  • Rows are sorted, selected for bulk actions, paged, or opened.
  • The data comes from a server that sorts and pages (manualSorting, manualPagination).

Avoid when

  • Label/value pairs about one record: use DescriptionList.
  • A list without columns: use a list of rows or cards.
  • Spreadsheet-style editing of every cell: that needs a dedicated editable grid.

Design principles

  • Native <table> semantics first: caption, scope="col" headers, the first column as row headers (scope="row"), and aria-sort on the primary sorted column.
  • No business logic: the app owns data, filters and persistence. DataTable sorts and pages client-side only when the app has not taken that over, and always reports intent through callbacks.
  • Every state is controlled or uncontrolled: sort/defaultSort, selectedRowIds/defaultSelectedRowIds, page/defaultPage, expandedRowIds/defaultExpandedRowIds, columnWidths/defaultColumnWidths.
  • Built from Monarch parts: Checkbox, DropdownMenu, Pagination, EmptyState, LoadingState and ErrorState, so states look the same as everywhere else.
  • Every control has a row-specific name ("Select INV-1042 for Northfield Traders", "Actions for …").

API reference

import { DataTable, DataTableColumnChooser, SortButton, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, TableSelectCell, columnChooserColumns, defaultColumnChooserMessages, defaultDataTableMessages, nextSort, sortRows, useDataTableColumns } from '@rookcom/monarch-react/data';
ExportKindRenderingRoot class
DataTablecomponentclient.mn-grid-skip
DataTableColumnChoosercomponentclient.mn-grid-skip
SortButtoncomponentclient.mn-table-wrap
Tablecomponentclient.mn-table-wrap
TableBodycomponentclient.mn-table-wrap
TableCaptioncomponentclient.mn-table-wrap
TableCellcomponentclient.mn-table-wrap
TableHeadcomponentclient.mn-table-wrap
TableHeadercomponentclient.mn-table-wrap
TableRowcomponentclient.mn-table-wrap
TableSelectCellcomponentclient.mn-table-wrap
columnChooserColumnsutilityclient–
defaultColumnChooserMessagesutilityclient–
defaultDataTableMessagesutilityclient–
nextSortutilityclient–
sortRowsutilityclient–
useDataTableColumnshookclient–

DataTable, DataTableColumnChooser, useDataTableColumns, sortRows, nextSort and columnChooserColumns are imported from @rookcom/monarch-react/data. The Table* parts and SortButton are exported from both @rookcom/monarch-react (unchanged) and @rookcom/monarch-react/data.

DataTable is generic over the row type: DataTable<Invoice> checks columns, getRowId and every callback against it. Its ref is the outer div.

DataTableColumn<T>: id, header, optional label, cell, accessor, sortable, sortFn, numeric, width/minWidth/maxWidth, resizable, hideable, defaultHidden, hideBelow, rowHeader.

sortRows(rows, sort, columns) is the client-side sort DataTable uses (stable, numeric-aware, empty values last); nextSort(sort, id, additive) is the header cycle (ascending → descending → off).

messages replaces any user-visible string, for translation.

Table gains two additive props: stickyFirstColumn and maxHeight.

Generated from the TypeScript declarations of packages/react/src/components/data-grid/data-table.

Properties

DataTable

A data table driven by column definitions: sorting (single or multi), row selection with select-all-on-page, column visibility, order and resizing, sticky header and first column, pagination (client or server), expandable rows, row actions, built-in loading, empty and error states, keyboard grid navigation and a stacked card layout for narrow screens. Built on the Table parts.

PropTypeDescription
caption requiredstringAccessible name of the table. Rendered as a `<caption>`.
columns requiredreadonly DataTableColumn<T>[]
getRowId required(row: T, index: number) => stringStable id of a row.
rows requiredreadonly T[]
allMatchingSelectedbooleanEvery matching row (not just this page) is selected; shows the banner state.
cardBreakpointnumberWidth in px below which `layout="auto"` shows cards. Default 640.
classNamestring
columnOrderreadonly string[]Column ids in display order (controlled). Unknown ids are ignored, missing ones appended.
columnVisibilityReadonly<Record<string, boolean>>Visible columns by id (controlled). Missing ids fall back to `!defaultHidden`.
columnWidthsReadonly<Record<string, number>>Column widths in px by id (controlled).
defaultColumnOrderreadonly string[]
defaultColumnVisibilityReadonly<Record<string, boolean>>Initial visibility (uncontrolled; change it with `columnVisibility` and a DataTableColumnChooser).
defaultColumnWidthsReadonly<Record<string, number>>
defaultExpandedRowIdsreadonly string[]
defaultPagenumber
defaultSelectedRowIdsreadonly string[]Initially selected row ids (uncontrolled).
defaultSortreadonly DataTableSort[]Initial sort (uncontrolled).
density"compact" | "comfortable"
emptyStateReactNodeReplaces the built-in empty state.
errorReactNodeLoading failed. A string or node becomes the description. Without rows it replaces the body.
expandedRowIdsreadonly string[]
filteredbooleanThe rows are empty because of filters: shows the filtered empty state.
footerReactNodeContent under the table, e.g. a LoadMore button.
getRowLabel((row: T) => string)Plain-text name of a row for checkbox, menu and expand labels ("Select Acme"). Default: the id.
idstring
isRowExpandable((row: T) => boolean)
isRowSelectable((row: T) => boolean)Rows that cannot be selected get a disabled checkbox.
keyboardNavigationbooleanKeyboard grid navigation (WAI-ARIA grid): the table becomes `role="grid"` with one tab stop and arrow-key movement between cells. Default: on when `onRowOpen` is set.
layout"table" | "auto" | "cards"`table` scrolls sideways on narrow screens; `cards` always renders a stacked list; `auto` switches to cards below `cardBreakpoint`.
loadingbooleanRows are loading. Without rows a skeleton fills the body; with rows they stay and the table is busy.
manualPaginationbooleanThe rows are already the current page (server-side pagination). Needs `totalRowCount`.
manualSortingbooleanThe rows arrive sorted (server-side sorting): the table only reports sort intent.
maxHeightstring | numberMaximum height of the scroll area.
messagesPartial<DataTableMessages>
multiSortbooleanShift+click (or Shift+Enter) adds a column to the sort. Default false.
onClearFilters(() => void)
onColumnWidthsChange((widths: Record<string, number>) => void)
onExpandedRowIdsChange((ids: string[]) => void)
onPageChange((page: number) => void)
onRetry(() => void)
onRowOpen((row: T) => void)Opens a row: click anywhere that is not a control, or Enter on a focused cell.
onSelectAllMatching(() => void)Called by "Select all N" in the selection banner. The app records that every match is selected.
onSelectedRowIdsChange((ids: string[]) => void)
onSortChange((sort: DataTableSort[]) => void)
pagenumber1-based page (controlled).
pageSizenumberRows per page. Adds a Pagination footer.
renderExpanded((row: T) => ReactNode)Detail content of an expanded row. Adds a leading expand column.
rowActions((row: T) => DataTableRowAction[])Actions for a row: inline buttons and an overflow menu in a trailing column.
selectablebooleanAdds a checkbox column.
selectedRowIdsreadonly string[]Selected row ids (controlled).
showCaptionbooleanShow the caption visually. Default false (it stays available to assistive technology).
sortreadonly DataTableSort[]Sort state (controlled).
stickyFirstColumnbooleanKeep the selection/expand columns and the first data column visible while scrolling sideways.
stickyHeaderbooleanKeep the header visible while the body scrolls. Set `maxHeight` too.
totalRowCountnumberRows matching the current filters across all pages (server-driven). Enables "select all N".

DataTableColumnChooser

Popover to show, hide and reorder DataTable columns. Reordering uses buttons, so it works by keyboard and screen reader without drag and drop.

PropTypeDescription
columns requiredreadonly DataTableColumnChooserColumn[]
onVisibilityChange required(visibility: Record<string, boolean>) => void
visibility requiredReadonly<Record<string, boolean>>Visible columns by id; missing ids count as visible.
classNamestring
messagesPartial<DataTableColumnChooserMessages>
onOrderChange((order: string[]) => void)
onReset(() => void)Adds a "Reset columns" button.
orderreadonly string[]Current order. With `onOrderChange`, each row gets move up/down buttons.
size"xs" | "sm" | "md"Trigger size. Default `sm`.

SortButton

Column sort toggle; its accessible name states the current direction.

PropTypeDescription
children requiredimport("/home/runner/work/monarch/monarch/node_modules/@types/react/index").ReactNode
direction requiredSortDirection
onClick required() => void

Table

Data table primitives. Wrap in `<Card flush>` for a framed table. Pass a `<TableCaption>` or `aria-label` so the table has an accessible name.

PropTypeDescription
childrenReactNode
classNamestring
density"compact" | "comfortable"Row height. `compact` for dense data, `comfortable` (default) for mixed content.
idstring
maxHeightstring | numberMaximum height of the scrolling wrapper, e.g. `480` or `'60vh'`. Pair with `stickyHeader`.
stickyFirstColumnbooleanKeep the first column visible while the table scrolls sideways. Cells can also opt in one by one with `data-pinned` (DataTable does this for its selection and expand columns).
stickyHeaderbooleanKeep the header visible while the body scrolls (the wrapper needs a height, see `maxHeight`).

TableBody

PropTypeDescription
childrenReactNode
classNamestring
idstring

TableCaption

PropTypeDescription
childrenReactNode
classNamestring
idstring

TableCell

PropTypeDescription
childrenReactNode
classNamestring
idstring
numericboolean

TableHead

PropTypeDescription
childrenReactNode
classNamestring
idstring
numericbooleanRight-align numbers.
onSortChange((next: "ascending" | "descending") => void)Called when the sort button is pressed. Required when `sort` is set.
sortSortDirectionCurrent sort of this column. Renders a SortButton and sets aria-sort.

TableHeader

PropTypeDescription
childrenReactNode
classNamestring
idstring

TableRow

PropTypeDescription
childrenReactNode
classNamestring
idstring
interactivebooleanPointer affordance for rows that open something on click. Keep a real link or button inside.
selectedbooleanHighlights the row (also sets aria-selected).

TableSelectCell

Narrow checkbox column for row selection.

PropTypeDescription
label requiredstringAccessible name, e.g. "Select ticket 42" or "Select all".
asChildboolean
childrenReactNode
classNamestring
disabledboolean
headerbooleanRender as a header cell (for the select-all checkbox).
idstring
invalidboolean

Variants

PropValuesDescription
density'comfortable''compact'Row height. Default comfortable. Exposed as data-density on the table.
layout'table''cards''auto'table (default) scrolls sideways when narrow; cards renders a stacked list; auto switches to cards below cardBreakpoint (640px of the table’s own width). Exposed as data-view.
stickyHeader / stickyFirstColumn'true''false'Keep the header row, and the selection/expand columns plus the first data column, visible while scrolling. Give maxHeight for a vertical scroll area.
multiSort'true''false'Shift+click or Shift+Enter on a header adds it to the sort; headers show their sort position.
manualSorting / manualPagination'true''false'Server-driven mode: rows arrive sorted and paged; DataTable only reports onSortChange and onPageChange. Pass totalRowCount.
numeric (column)'true''false'Right-aligns the header and cells with tabular figures (data-numeric).

States

StateExposed asBehaviour
Loadingaria-busy, data-busyWithout rows the body shows a LoadingState skeleton; with rows they stay, a progress line runs above them and the table is aria-busy. "Loading rows" is announced.
Empty / filtered empty–An EmptyState fills the body. With filtered it explains that filters hide everything and offers onClearFilters. emptyState replaces it.
Error–Without rows an ErrorState with "Try again" (onRetry) replaces the body; with rows a compact one appears above them.
Sorted columnaria-sort, data-directionThe primary sorted column has aria-sort; the sort button’s name states the direction (and position in a multi-sort).
Selected rowaria-selected, data-selectedSelected rows are highlighted. The header checkbox is checked, unchecked or mixed for the current page.
All matching selected–With totalRowCount and onSelectAllMatching, selecting the whole page shows "Select all N"; allMatchingSelected shows "All N rows are selected" with "Clear selection".
Expanded rowaria-expanded, data-expandedA detail row follows the row, linked by aria-controls.

Interaction behaviour

  • A sortable header cycles ascending → descending → unsorted. With multiSort, Shift adds or updates a secondary sort.
  • Row checkboxes select one row; Shift+click selects the range since the last clicked row. The header checkbox selects or clears every selectable row on the page.
  • With onRowOpen, clicking a table row anywhere except a control (or while text is selected) opens it; in the card layout each card has an explicit “Open …” button.
  • Row actions marked inline are visible buttons; the rest are in an overflow menu.
  • Resizable columns are dragged by their handle; double-click resets to the default width.
  • DataTableColumnChooser shows, hides and reorders columns with checkboxes and move buttons (no drag and drop), and can reset them.
  • Client-side sorting and paging happen when manualSorting / manualPagination are off.

Keyboard support

KeysAction
TabWithout grid navigation: moves through sort buttons, resize handles, checkboxes and row controls. With grid navigation: enters and leaves the table (one tab stop).
Arrow keysGrid navigation: move one cell (RTL aware).
Home / EndGrid navigation: first / last cell in the row.
Ctrl+Home / Ctrl+EndGrid navigation: first / last cell of the table.
Page Up / Page DownGrid navigation: move five rows.
EnterOn a cell: opens the row (onRowOpen), or moves into a cell with several controls. On a sort button: sorts (Shift+Enter adds to a multi-sort).
SpaceOn a row cell: toggles its selection. On a checkbox: toggles it.
EscapeLeaves a cell with several controls, back to grid navigation.
Arrow Left / Right, Home / EndOn a focused resize handle: narrower / wider by 16px (64px with Shift), minimum / maximum. In grid navigation use Alt+Arrow on the header cell.

Grid navigation (WAI-ARIA grid pattern) is on by default when onRowOpen is set; turn it on or off with keyboardNavigation. A cell with one control focuses that control directly; text inputs inside cells keep their arrow keys.

Accessibility

Role
table, or grid with keyboard navigation
Accessible name
caption (required), rendered as a <caption>; visually hidden unless showCaption.
  • The first visible column renders as row headers (th scope="row") so every cell is announced with its row.
  • aria-sort is set on one header at a time (the primary sort), as ARIA recommends.
  • Utility columns (expand, select, actions) have visually hidden header text, so no header cell is empty.
  • Hidden columns (visibility or hideBelow) are removed from the accessibility tree too; the card layout shows every visible column as a labelled field.
  • The resize handle is a focusable separator with aria-valuenow/min/max.
  • Loading is announced through a polite status region; selection counts are announced by BulkActionBar.
  • Card layout is a labelled list; each card has a labelled checkbox and an "Open …" button, so it never relies on a clickable container.

Responsive behaviour

  • Default: the table scrolls sideways inside its wrapper; with stickyFirstColumn the row name stays visible at 320px.
  • hideBelow on a column hides it while the table is narrower than that width (responsive column priority).
  • layout="auto" switches to stacked cards under cardBreakpoint, measured on the table itself (works inside split panes); layout="cards" always uses them.
  • Pagination switches to its compact variant in the card layout.

Theme support

  • Borders, hover, selected, pinned-column and banner backgrounds use Monarch semantic tokens, so light, dark and high-contrast themes apply.
  • The loading line stops animating with prefers-reduced-motion: reduce; expand and resize transitions are removed.
  • Wrap in <Card flush> for a framed table.

Do and don't

Do

  • Give every table a caption and rows a getRowLabel, so controls are named after the row.
  • Use manualSorting and manualPagination when the server holds more rows than the page.
  • Pair selection with BulkActionBar, and filters with FilterBar and filtered for the empty state.
  • Mark numeric columns numeric and keep the record name in the first column.

Don't

  • Do not hide important columns with hideBelow without offering the card layout or a detail view.
  • Do not put several unrelated controls in one cell; give each its own column or a row action.
  • Do not use a table for page layout.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsButton, Checkbox, DropdownMenu, Popover, EmptyState, ErrorState, LoadingState, Pagination

No virtualisation: no product evidence needs more than a page of rows on screen.

Testing coverage

  • sortRows: stable, numeric-aware, empty last, multi-key; nextSort cycle and additive mode.
  • Caption, row headers and numeric cells; client sort with aria-sort on the primary column; multi-sort with Shift and priority in the name.
  • Server-driven mode reports sort and page without reordering; client pagination.
  • Row selection, select all on page (mixed state), Shift range, isRowSelectable, controlled selection, "select all matching" banner.
  • Column visibility, order and hideBelow; column chooser hides, reorders by button (focus kept) and resets.
  • Loading, error (retry), empty and filtered-empty states; rows stay while reloading.
  • Row actions with row-context names (inline and menu); expandable rows with aria-expanded/aria-controls.
  • Row click opens except on controls; grid keyboard: one tab stop, arrows, Home/End, Ctrl+End, Enter opens, Space selects.
  • Keyboard column resizing; pinned columns; card layout with labelled fields; server rendering.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesGallery visual baselines (every example) and cross-browser axe on #/components/data-table.

Tests: packages/react/src/components/data-grid/data-table