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 withpagination.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"withselectionActionsfor bulk actions;footerin a column to sum what remains after filtering.renderExpandedRowturns each row into an expandable one, with its detail at full width below;maxHeightfixes 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
groupBygroups by a column, with a collapsible header per group (initialCollapsedGroups,renderGroupLabel).advancedFiltersadds a panel of "column · operator · value" conditions joined by "and" or "or" (text, numbers, dates and yes/no, with "between" and "is empty");advancedFiltercontrols it from outside.exportCsvdownloads 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: falseleaves a column out.persistKeyremembers visible columns, order and widths in the browser; the columns panel offers "Reset".editoron a column +onCellEditon the table: Enter, F2 or double click open the cell; Enter saves, Escape cancels, leaving saves. IfonCellEditfails, the field stays open with the error. The table does not changerows: whoever uses it updates their data.virtualized+maxHeightpaints 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 withrenderExpandedRow.
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
| Order | Customer | Status | Total |
|---|---|---|---|
| 1041 | Aitor Etxeberria | Shipped | €1,249.00 |
| 1039 | Javier Ortega | Shipped | €420.00 |
| 1043 | Pablo Gil | Delivered | €315.50 |
| 1038 | Lucía Martín | Delivered | €189.00 |
| 1042 | Nerea Sánchez | Pending | €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.
| Prop | Type | What it does |
|---|---|---|
columnsrequired | readonly TableColumn<T>[] | — |
labelrequired | string | What is announced on entering the table. Required: without this it is just "table". |
rowKeyrequired | (row: T) => string | — |
rowsrequired | readonly T[] | — |
advancedFilter | AdvancedFilter | The advanced filter, controlled from outside. Without it, the table keeps it. |
advancedFilters | boolean | Adds the panel of filters with several conditions joined by "and" / "or". |
cellSelection | boolean | Cell 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. |
className | string | Merged with the library classes; it does not replace them. |
configurableColumns | boolean | Adds the panel to show, hide and reorder columns. |
emptyMessage | string | — |
error | ReactNode | Loading 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. |
expandedKeys | readonly string[] | Expanded rows (by their key), controlled from outside. Without them, the table keeps track of them. |
exportCsv | boolean | 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. |
exportXlsx | boolean | 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. |
filters | Readonly<Record<string, string | readonly string[]>> | Filters controlled from outside. Without them, the table keeps them. |
groupBy | string | Groups the rows by the value of this column, with a collapsible header row per group. |
groupTotals | boolean | With `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). |
initialCollapsedGroups | readonly string[] | Groups collapsed at the start (by the value of their key). |
initialExpandedKeys | readonly string[] | The rows expanded at the start, without controlling them from outside. |
initialSort | TableSort | null | — |
loading | boolean | — |
maxHeight | string | Maximum 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. |
multiSort | boolean | With `true`, Shift+click on another header adds it as a tie-breaking sort criterion. |
onAdvancedFilterChange | (filter: AdvancedFilter) => void | Notifies 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) => void | Notifies of the active cell range (the last one started), or of `null` when they are cleared. |
onCellRangesChange | (ranges: readonly CellRange[]) => void | Notifies 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[]) => void | Reports with the expanded keys. It only reports: it does not control or turn anything off. |
onFilter | (filters: TableFilters) => void | Notifies of the new filters. It only notifies: it does not control or turn anything off. |
onRetry | () => void | Requests the data again after an `error`. |
onRowPress | (row: T) => void | Notifies of the row pressed. Expanding the detail does not count. |
onSearchChange | (text: string) => void | Reports 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[]) => void | Notifies with the selected keys. It only notifies: it controls nothing. |
onSort | (sort: TableSort | null) => void | Announces the new order, or `null` when it is removed. It only announces: it does not control or turn anything off. |
onSortsChange | (sorts: readonly TableSort[]) => void | Notifies with the full list of sort criteria (the first one is the one `onSort` reports). |
onVisibleColumnsChange | (keys: readonly string[]) => void | Reports the visible columns, in their order. It only reports: it controls nothing. |
pagination | false | TablePagination | — |
persistKey | string | Remembers 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) => ReactNode | Turns 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[]) => ReactNode | What the header of a group says. By default, the value and the count. |
resizableColumns | boolean | The edge of each header can be dragged to change the width of the column. |
rowClassName | (row: T, index: number) => string | undefined | A 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) => boolean | Which rows can be expanded. All by default. |
searchable | boolean | Puts a search box above the table that filters by the text of any visible column. |
selectedKeys | readonly string[] | Selected keys, controlled from outside. Without them, the table keeps them. |
selection | SelectionMode | — |
selectionActions | (keys: readonly string[]) => ReactNode | Bar that appears when rows are selected. |
serverSide | boolean | Sorting 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. |
sort | TableSort | null | Sort order controlled from outside. Without it, the table manages it, starting at `initialSort`. |
toolbar | ReactNode | Custom content in the top bar (search field, export button...). |
tree | TableTree<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`. |
virtualized | boolean | TableVirtualization | Paints 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. |
visibleColumns | readonly 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.
| Variable | Default |
|---|---|
--amset-table-row | var(--amset-space-2) |
--amset-table-cell | var(--amset-space-3) |
--amset-table-font | var(--amset-text-base) |
--amset-table-column | var(--amset-text-sm) |
--amset-table-radius | var(--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:
