DataTable
A table of records: columns with minimum widths and shares, selection, sorting, row actions, a sticky header and first column, skeleton rows and pagination.
import { DataTable } from "@butteryui/react";Demo
When to use it
Use it for
- Lists of records people scan, sort and act on: orders, customers, payouts
- A short list in an overview (size="compact")
Not for
- Items grouped under headings with an add row: use GroupedList
- A handful of settings: use an Inspector or a form
- Content cards: use Grid
Examples
Orders table
An orders page: selection with a count, sortable headers and a menu of actions on each row.
Orders
This week
0 selected · No changes yet
Orders that load
Skeleton rows while the orders load, then a row with Clear all when a search leaves none.
Payouts
The least a table needs: a label, its columns and its rows, numbers aligned to the end.
Recent activity
Both sizes: compact for a short list in an overview, with 32px rows and no selection or pagination, and the default with them.
Roast log
A roast log drawn plain, striped or bordered, picked with a segmented control.
Variants and states
Orders
Select rows, sort by customer or total, open a row's actions, page through, or reload for the skeleton rows.
No results
Striped
variant striped puts every second row on bg-section, with selection, sorting, row actions, pagination and the skeleton rows working as in a plain table; hover takes bg-control so it shows on either.
Bordered
variant bordered draws a line between the cells inside a 1px frame with 12px corners. Given a height, the table scrolls inside the frame with its header stuck to the top; here it's compact too.
Props
DataTable
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | Names the table for screen readers, such as "Orders". | |
columnsrequired | readonly DataTableColumn<Row>[] | The columns, in order. | |
rowsrequired | readonly Row[] | The rows to show: the current page, sorted and filtered as you want them. | |
rowKeyrequired | (row: Row) => string | A stable key for each row, also what selection holds. | |
rowLabel | ((row: Row) => string) | A short name for each row, for its checkbox and its actions, such as the order number. | |
selectable | boolean | false | Adds a checkbox column. Defaults to false; size="compact" never has one. |
selected | ReadonlySet<string> | The selected rows' keys. Pair with onSelectedChange to control them. | |
defaultSelected | ReadonlySet<string> | The rows selected at first when uncontrolled. | |
onSelectedChange | ((selected: ReadonlySet<string>) => void) | Called with the new selection. | |
sort | DataTableSort | null | The current sort, or null for none. Sorting the rows is up to you. | |
onSortChange | ((sort: DataTableSort | null) => void) | Called when a sortable header is pressed: ascending, then descending, then none. | |
rowActions | ((row: Row) => ReactNode) | A row's actions, as Menu.Items, behind a "…" button in its last cell. Return null for a row with
nothing to do: its cell stays empty, with no button. | |
loading | boolean | Shows skeleton rows shaped like each column while the rows load. | |
pagination | DataTablePagination | The pagination under the table. size="compact" never has one. | |
empty | ReactNode | What shows in the table's place when there's no data at all, usually an EmptyState. | |
onClearAll | (() => void) | When filters leave no rows: a row across the table with "Clear all", which calls this. | |
size | "default" | "compact" | "default", or "compact" for short lists such as recent activity: the table takes compact density's
heights, so its rows are 32px whatever the app's density, and has no selection or pagination. | |
variant | "plain" | "striped" | "bordered" | "plain" | How it draws its rows: "plain" (the default), a 1px divider under each; "striped", every second body row
on bg-section as well, with hover taking bg-control so it shows on either; or "bordered", a 1px line
between the cells too, inside a 1px frame with 12px corners. Each works at either size, with selection,
sorting and the sticky header and first column. |
labels | Partial<DataTableLabels> | Words to use instead of the English ones. |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
DataTable.Amount
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | The amount. | |
currencyrequired | string | Its ISO 4217 currency, such as "USD": the number shows with the currency's symbol, and the code after
it in text-tertiary, as in "$14.50 USD". |
It also takes Omit<ComponentPropsWithRef<"span">, "children">.
DataTable.Id
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | The identifier, such as an order's. |
It also takes Omit<ComponentPropsWithRef<"span">, "children">.
DataTable.Link
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the single child element (such as a router's link) as the link, with these styles merged in. |
It also takes ComponentPropsWithRef<"a">.
DataTableColumn
One column of a `DataTable`.
| Field | Type | Default | Description |
|---|---|---|---|
idrequired | string | Names the column for sorting and keys. | |
headerrequired | string | The header's text. | |
cellrequired | (row: Row) => ReactNode | What a row shows in this column. Text truncates with its full text on hover. | |
minWidth | "xs" | "sm" | "md" | "lg" | "xl" | "sm" | The least the column takes before the table scrolls sideways. Defaults to "sm". |
grow | number | 1 | Its share of the table's width against the other columns' grow, never less than its least width. Defaults to 1. |
align | "start" | "end" | "start" | "start" (the default), or "end" for numbers, which also sets them tabular. |
sortable | boolean | Whether the header sorts the table. | |
skeleton | "text" | "badge" | "number" | "text" | What its cells look like while rows load. Defaults to "text" ("number" when aligned to the end). |
DataTableSort
Which column sorts the table, and which way.
| Field | Type | Default | Description |
|---|---|---|---|
columnrequired | string | The id of the column that sorts the table. | |
directionrequired | "ascending" | "descending" | Which way: "ascending" or "descending". |
DataTablePagination
Where the rows shown sit among all of them, for the pagination under the table.
| Field | Type | Default | Description |
|---|---|---|---|
pagerequired | number | The page shown, from 1. | |
pageSizerequired | number | How many rows a page holds. | |
totalrequired | number | How many rows there are in all. | |
onPageChangerequired | (page: number) => void | Called with the page Previous or Next goes to. |
DataTableLabels
The words a `DataTable` uses, for other languages.
| Field | Type | Default | Description |
|---|---|---|---|
selectAllrequired | string | Names the header's checkbox, which selects the page. | |
selectRowrequired | (row: string) => string | Names a row's checkbox, from the row's label. | |
actionsrequired | string | Names the actions column for screen readers. | |
actionsForrequired | (row: string) => string | Names a row's "…" button, from the row's label. | |
previousrequired | string | The button that goes back a page. | |
nextrequired | string | The button that goes on a page. | |
rangerequired | (first: number, last: number, total: number) => string | Which rows show, such as "1–14 of 42". | |
noResultsrequired | string | The title of the row shown when filters leave nothing. | |
noResultsBodyrequired | string | The line under it. | |
clearAllrequired | string | The button that clears the filters. | |
loadingrequired | string | What screen readers hear while the rows load. |
Accessibility
ARIA table roles on grid rows: a table named by its label, columnheaders with aria-sort on sortable ones (buttons inside), cells. Row checkboxes are named Select and the row's label; the header box selects the page and reads mixed when some are. A row's actions button is named Actions for and the row's label. While loading, the table is aria-busy and says it's loading rows. Its scroll area is a region named by the label, and takes focus while its columns or rows overflow, so the keyboard can scroll it.
| Keys | What they do |
|---|---|
| Tab or Shift+Tab | Moves through its checkboxes, sortable headers, links, row actions and pagination, and onto its scroll area while it overflows |
| Space | Ticks or clears the focused checkbox; the header's selects the whole page |
| Enter or Space | Sorts by the focused header (ascending, then descending, then none), or opens a row's actions |
| Arrow keys | Scroll the table while its scroll area has focus |
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: Skeleton rows don't shimmer, and the rows that replace them fade in over 100ms.
Layout
It fills its container's width. Each column keeps its least width (minWidth: xs 64, sm 96 by default, md 144, lg 192 or xl 256px) and a share of the rest, and when those don't fit the table scrolls sideways inside itself while the page doesn't, its end edge fading while more lies past it; rows are 36px (--bt-h-table-row; 32 in a compact table) with 1px dividers, every second one on bg-section when striped and with a 1px line between cells inside a 1px frame with 12px corners when bordered, neither changing a row's height; cells never wrap but truncate with their full text on hover, and the header and first column stick as it scrolls. Give an amount or a date column minWidth md, so its values fit whole on a phone.
Related
See it in the roastery demo's customers, orders, payouts, subscriptions and wholesale.