Skip to content
EspañolEnglish
All components (244)

Table

Data · Table · .amset-table

What it is and when to use it

Data compared by columns (invoices, orders, customers), with sorting, per-column filters, selection, pagination, configurable columns and totals at the bottom.

When to use it

  • Management lists where you search, sort and act on rows.
  • It sorts, filters, selects and paginates on its own. The callbacks (onSort, onFilter, onSelectionChange, onVisibleColumnsChange, pagination.onPageChange) only notify: each part is controlled by whoever uses it only if they also pass its value (sort, filters, selectedKeys, visibleColumns, pagination.page). These pages pass it all the callbacks and the table keeps working on its own.
  • With serverSide (or with pagination.total) it does not sort or filter locally: it draws the rows as they arrive, and whoever uses it asks the server for what the notifications say.
  • selection="multiple" with selectionActions for bulk actions; footer in a column to sum what remains after filtering.
  • renderExpandedRow turns each row into an expandable one, with its detail at full width below; maxHeight fixes the height and keeps the header and the footer still.
  • Without pagination and inside a container with data-density="compact" on a dashboard: density is global, not a prop.

When not to

  • The data of a single thing, label and value: DataList.
  • Thousands of rows that are scrolled through continuously, without columns: VirtualList.
  • A comparison of plans or prices: PricingTable.
  • A full spreadsheet (formulas, range selection, mass paste): the editing here is cell by cell.

Beyond the basics

  • groupBy groups by a column, with a collapsible header per group (initialCollapsedGroups, renderGroupLabel).
  • advancedFilters adds a panel of "column · operator · value" conditions joined by "and" or "or" (text, numbers, dates and yes/no, with "between" and "is empty"); advancedFilter controls it from outside.
  • exportCsv downloads as CSV every row, already filtered and sorted, in the visible columns (with a BOM for Excel, the language's separator and text that looks like a formula neutralised). exportable: false leaves a column out.
  • persistKey remembers visible columns, order and widths in the browser; the columns panel offers "Reset".
  • editor on a column + onCellEdit on the table: Enter, F2 or double click open the cell; Enter saves, Escape cancels, leaving saves. If onCellEdit fails, the field stays open with the error. The table does not change rows: whoever uses it updates their data.
  • virtualized + maxHeight paints only the rows that are visible, for thousands of rows (Home, End and the page keys jump even if the row is not painted). It does not combine with renderExpandedRow.

Keyboard: the table is a single Tab stop. Up Arrow and Down Arrow go from row to row, keeping the column; Right Arrow enters the cells (to the control they have inside, such as the selection checkbox, which is checked with Space) and Left Arrow goes back to the row. Home and End go to the first and the last row. Pressing the header of a sortable column sorts (with the focus on it, also Enter or Space): ascending, descending and unsorted. The funnel in the header opens the filter (in the text one, Enter filters) and "Columns" opens a panel with checkboxes and move up and move down buttons, without dragging anything.

Accessibility: it is a real <table> with a grid role (role="grid"), a required name (label), headers with aria-sort and, with rowHeader, the column that names each row. With the filter applied, the button changes its name ("Filter for Customer (active)") and its shape, not just its color: the funnel fills in, just as the active sort turns off one of its two arrows. Both are identical 22 px boxes next to the title. The selection bar ("3 rows selected") and the page count go in role="status". While loading (loading) it carries aria-busy and, if there are no rows yet, says "Loading…".

Fine-tuning: CSS variables --amset-table-*: -cell, -column, -font, -padding-row, -radius, -row. The size changes the cell padding and the font size. The vertical padding is set by density (data-density) when there is one.

What it looks like

OrderCustomerStatusTotal
1041Aitor EtxeberriaShipped€1,249.00
1039Javier OrtegaShipped€420.00
1043Pablo GilDelivered€315.50
1038Lucía MartínDelivered€189.00
1042Nerea SánchezPending€89.00

<Table
  label="October orders"
  columns={[
    { key: 'id', title: 'Order', rowHeader: true },
    { key: 'cliente', title: 'Customer', sortable: true, filterable: 'text' },
    { key: 'estado', title: 'Status', filterable: 'list' },
    { key: 'total', title: 'Total', numeric: true, sortable: true },
  ]}
  rows={pedidos}
  rowKey={(pedido) => pedido.id}
  initialSort={{ column: 'total', direction: 'desc' }}
  pagination={{ size: 5, sizes: [5, 10] }}
/>

Props

Import it with import { Table } from '@amset/ui'. Required props are marked.

PropTypeWhat it does
columnsrequiredreadonly TableColumn<T>[]—
labelrequiredstringWhat is announced on entering the table. Required: without this it is just "table".
rowKeyrequired(row: T) => string—
rowsrequiredreadonly T[]—
advancedFilterAdvancedFilterThe advanced filter, controlled from outside. Without it, the table keeps it.
advancedFiltersbooleanAdds the panel of filters with several conditions joined by "and" / "or".
cellSelectionbooleanCell selection as in a spreadsheet: one range by dragging, with Shift+click or Shift+arrows, Ctrl/Cmd+A for everything and Escape to clear it; several loose ranges with Ctrl/Cmd+click (on a cell already selected, it removes it) or with Shift+F8 from the keyboard. Ctrl/Cmd+C copies as TSV (and as an HTML table): several ranges only if they share rows or columns, as in Excel. With `onCellEdit`, Ctrl/Cmd+V pastes from the starting cell downwards and to the right, only in the columns with `editor`; a single value is spread over everything selected, also when there are several ranges.
classNamestringMerged with the library classes; it does not replace them.
configurableColumnsbooleanAdds the panel to show, hide and reorder columns.
emptyMessagestring—
errorReactNodeLoading the data failed: this reason is shown instead of the rows (or `true` for the default text), announced as an alert. With `onRetry` it carries a button to request them again.
expandedKeysreadonly string[]Expanded rows (by their key), controlled from outside. Without them, the table keeps track of them.
exportCsvboolean | TableExportOptions<T>Adds a button that downloads as CSV what shows in the visible columns: every row, already filtered and sorted, not just the page. With `serverSide` it only exports the loaded rows.
exportXlsxboolean | TableExportOptions<T>Adds a button that downloads to Excel (.xlsx) the same as the CSV, with each value with its type: numbers as numbers and dates as dates. The writer is loaded on pressing, not with the table. `separator` is not used here.
filtersReadonly<Record<string, string | readonly string[]>>Filters controlled from outside. Without them, the table keeps them.
groupBystringGroups the rows by the value of this column, with a collapsible header row per group.
groupTotalsbooleanWith `groupBy`, a totals row at the end of each group with each column's `footer` computed over the rows of THAT group (all of them, even if the page only shows part).
initialCollapsedGroupsreadonly string[]Groups collapsed at the start (by the value of their key).
initialExpandedKeysreadonly string[]The rows expanded at the start, without controlling them from outside.
initialSortTableSort | null—
loadingboolean—
maxHeightstringMaximum height of the table (`'24rem'`, `'60vh'`): beyond it, the body scrolls and the header and footer stay fixed. It is the `--amset-table-height` variable made a prop.
multiSortbooleanWith `true`, Shift+click on another header adds it as a tie-breaking sort criterion.
onAdvancedFilterChange(filter: AdvancedFilter) => voidNotifies of the new advanced filter. It only notifies: it controls nothing.
onCellEdit(edit: CellEdit<T>) => void | Promise<void>Saves what was edited in a cell with `editor`. It opens with Enter, F2 or double click and closes with Enter (saves), Escape (cancels) or leaving the field (saves). It can be asynchronous: if it throws, the field stays open with the error; if it fails when leaving the field, the edit is discarded and the table announces why. The table does NOT change `rows`: whoever uses it updates their data.
onCellRangeChange(range: CellRange | null) => voidNotifies of the active cell range (the last one started), or of `null` when they are cleared.
onCellRangesChange(ranges: readonly CellRange[]) => voidNotifies of all the loose ranges at once, from first to last, every time they change; empty when they are cleared.
onCellsEdit(edits: readonly CellEdit<T>[]) => void | Promise<void>When several cells are pasted at once (`cellSelection`), receives ALL the valid edits in a single call instead of one `onCellEdit` per cell: one save and one repaint. If it throws, none counts as pasted.
onExpandedChange(keys: readonly string[]) => voidReports with the expanded keys. It only reports: it does not control or turn anything off.
onFilter(filters: TableFilters) => voidNotifies of the new filters. It only notifies: it does not control or turn anything off.
onRetry() => voidRequests the data again after an `error`.
onRowPress(row: T) => voidNotifies of the row pressed. Expanding the detail does not count.
onSearchChange(text: string) => voidReports every change of the search text. With `serverSide` or server pagination the table cannot filter what it does not have, so whoever provides the data runs the search: without this, the box does not show in those modes.
onSelectionChange(keys: readonly string[]) => voidNotifies with the selected keys. It only notifies: it controls nothing.
onSort(sort: TableSort | null) => voidAnnounces the new order, or `null` when it is removed. It only announces: it does not control or turn anything off.
onSortsChange(sorts: readonly TableSort[]) => voidNotifies with the full list of sort criteria (the first one is the one `onSort` reports).
onVisibleColumnsChange(keys: readonly string[]) => voidReports the visible columns, in their order. It only reports: it controls nothing.
paginationfalse | TablePagination—
persistKeystringRemembers between visits which columns are shown, in what order and at what width, in the browser, under this key. With a controlled `visibleColumns` it does not remember the visible ones.
renderExpandedRow(row: T) => ReactNodeTurns every row into an expandable one and paints its detail here: an order with its lines, a customer with their history. Each row gains a button with `aria-expanded` at the start and the detail appears in a row below, full width. Rows with nothing to expand are marked with `rowExpandable`.
renderGroupLabel(value: CellValue, rows: readonly T[]) => ReactNodeWhat the header of a group says. By default, the value and the count.
resizableColumnsbooleanThe edge of each header can be dragged to change the width of the column.
rowClassName(row: T, index: number) => string | undefinedA class to mark a specific row (an overdue invoice in red) without touching the global CSS. It is ADDED to those of the table. `index` is the position in the page being shown.
rowExpandable(row: T) => booleanWhich rows can be expanded. All by default.
searchablebooleanPuts a search box above the table that filters by the text of any visible column.
selectedKeysreadonly string[]Selected keys, controlled from outside. Without them, the table keeps them.
selectionSelectionMode—
selectionActions(keys: readonly string[]) => ReactNodeBar that appears when rows are selected.
serverSidebooleanSorting and filtering are done by the server: the table paints `rows` as they arrive and only notifies through `onSort` and `onFilter`. With `pagination.total` it is so without asking, because sorting a single page locally makes no sense.
size"sm" | "md" | "lg"The size of the table: it moves the cell padding and the body font size. Not to be confused with `pagination.size`, which is the rows per page.
sortTableSort | nullSort order controlled from outside. Without it, the table manages it, starting at `initialSort`.
toolbarReactNodeCustom content in the top bar (search field, export button...).
treeTableTree<T>Hierarchical rows: each row can have child rows that fold under it (folders, ledger accounts, tasks with subtasks). `tree.children` says which they are. The table becomes a `treegrid`: each row announces its level and position, the right arrow opens, the left one folds or goes up to the parent. Sorting and filtering work level by level, and a filter shows the path to each match. It paginates the root rows. It is not combined with `groupBy`.
virtualizedboolean | TableVirtualizationPaints only the rows that are visible, for thousands of rows. It needs `maxHeight` (without a height there is no scrolling) and does not combine with `renderExpandedRow`, whose rows have different heights.
visibleColumnsreadonly string[]Visible columns and their order, controlled from outside. Without them, the table keeps track of them.

It also accepts the attributes of the element it renders: className, style, data-*, aria-* and its events.

CSS variables

These CSS variables are how you adapt it. Declare them on the component's selector, not on :root: the stylesheet redeclares them in the rule itself, and a declaration on the same element always wins.

VariableDefault
--amset-table-rowvar(--amset-space-2)
--amset-table-cellvar(--amset-space-3)
--amset-table-fontvar(--amset-text-base)
--amset-table-columnvar(--amset-text-sm)
--amset-table-radiusvar(--amset-radius-lg)

How to change it

.amset-table {
  --amset-table-row: ...;
  --amset-table-cell: ...;
}

In the test bench

Every state and variant, to try one at a time, on its page at ui.amset.tech: