Skip to content
EspañolEnglish
All components (204)

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".
classNamestringMerged with the library classes; it does not replace them.
configurableColumnsbooleanAdds the panel to show, hide and reorder columns.
emptyMessagestring—
expandedKeysreadonly string[]Expanded rows (by their key), controlled from outside. Without them, the table keeps track of them.
exportCsvboolean | TableExportOptionsAdds 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.
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.
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.
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. The table does NOT change `rows`: whoever uses it updates their data.
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.
onRowPress(row: T) => voidNotifies of the row pressed. Expanding the detail does not count.
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.
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.
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...).
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: