Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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

Order
Customer
Status
Total
ORD-2041
Ines Vale
Paid
$14.50USD
ORD-2042
Tomas Reyes
Unpaid
$51.50USD
ORD-2043
Mira Okafor
Paid
$88.50USD
ORD-2044
Lena Park
Paid
$35.50USD

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

Order
Status
Actions
ORD-2041
Ines Vale
Paid
$42.00USD
ORD-2042
Tomas Reyes
Unpaid
$18.50USD
ORD-2043
Mira Okafor
Paid
$96.00USD
ORD-2044
Lena Park
Paid
$27.25USD

Orders that load

Skeleton rows while the orders load, then a row with Clear all when a search leaves none.

Order
Customer
Loading rows

Payouts

The least a table needs: a label, its columns and its rows, numbers aligned to the end.

Paid out
Orders
Amount
Friday, April 12
38
$1,284.50USD
Friday, April 5
31
$1,032.00USD
Friday, March 29
42
$1,411.25USD

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.

Activity
When
Ines Vale paid ORD-2041
2 min ago
Guji came back in stock
1 hr ago
Tomas Reyes paused his plan
Yesterday

Roast log

A roast log drawn plain, striped or bordered, picked with a segmented control.

Coffee
Batch (kg)
Weight loss
Guji, Ethiopia
12
14.2%
Huila, Colombia
15
13.8%
Cerrado, Brazil
20
15.1%
Kiambu, Kenya
10
13.5%

Variants and states

Orders

Select rows, sort by customer or total, open a row's actions, page through, or reload for the skeleton rows.

Order
Status
Actions
ORD-2041
Ines Vale
Paid
$14.50USD
ORD-2042
Tomas Reyes
Unpaid
$51.50USD
ORD-2043
Mira Okafor
Paid
$88.50USD
ORD-2044
Lena Park
Paid
$35.50USD
ORD-2045
Omar Haddad
Unpaid
$72.50USD
ORD-2046
Ana de la Cruz
Paid
$19.50USD
ORD-2047
Kofi Mensah
Paid
$56.50USD
ORD-2048
Ines Vale
Unpaid
$93.50USD
1–8 of 23

No results

Order
Status
No resultsTry other filters or a different search.

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.

Order
Status
Actions
ORD-2041
Ines Vale
Paid
$14.50USD
ORD-2042
Tomas Reyes
Unpaid
$51.50USD
ORD-2043
Mira Okafor
Paid
$88.50USD
ORD-2044
Lena Park
Paid
$35.50USD
ORD-2045
Omar Haddad
Unpaid
$72.50USD
ORD-2046
Ana de la Cruz
Paid
$19.50USD
ORD-2047
Kofi Mensah
Paid
$56.50USD
ORD-2048
Ines Vale
Unpaid
$93.50USD
1–8 of 23

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.

Order
Status
ORD-2041
Ines Vale
Paid
$14.50USD
ORD-2042
Tomas Reyes
Unpaid
$51.50USD
ORD-2043
Mira Okafor
Paid
$88.50USD
ORD-2044
Lena Park
Paid
$35.50USD
ORD-2045
Omar Haddad
Unpaid
$72.50USD
ORD-2046
Ana de la Cruz
Paid
$19.50USD
ORD-2047
Kofi Mensah
Paid
$56.50USD
ORD-2048
Ines Vale
Unpaid
$93.50USD
ORD-2049
Tomas Reyes
Paid
$40.50USD
ORD-2050
Mira Okafor
Paid
$77.50USD
ORD-2051
Lena Park
Unpaid
$24.50USD
ORD-2052
Omar Haddad
Paid
$61.50USD
ORD-2053
Ana de la Cruz
Paid
$98.50USD
ORD-2054
Kofi Mensah
Unpaid
$45.50USD
ORD-2055
Ines Vale
Paid
$82.50USD
ORD-2056
Tomas Reyes
Paid
$29.50USD
ORD-2057
Mira Okafor
Unpaid
$66.50USD
ORD-2058
Lena Park
Paid
$103.50USD
ORD-2059
Omar Haddad
Paid
$50.50USD
ORD-2060
Ana de la Cruz
Unpaid
$87.50USD
ORD-2061
Kofi Mensah
Paid
$34.50USD
ORD-2062
Ines Vale
Paid
$71.50USD
ORD-2063
Tomas Reyes
Unpaid
$18.50USD

Props

DataTable

PropTypeDefaultDescription
label

required

stringNames the table for screen readers, such as "Orders".
columns

required

readonly DataTableColumn<Row>[]The columns, in order.
rows

required

readonly Row[]The rows to show: the current page, sorted and filtered as you want them.
rowKey

required

(row: Row) => stringA 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.
selectablebooleanfalseAdds a checkbox column. Defaults to false; size="compact" never has one.
selectedReadonlySet<string>The selected rows' keys. Pair with onSelectedChange to control them.
defaultSelectedReadonlySet<string>The rows selected at first when uncontrolled.
onSelectedChange((selected: ReadonlySet<string>) => void)Called with the new selection.
sortDataTableSort | nullThe 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.
loadingbooleanShows skeleton rows shaped like each column while the rows load.
paginationDataTablePaginationThe pagination under the table. size="compact" never has one.
emptyReactNodeWhat 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.
labelsPartial<DataTableLabels>Words to use instead of the English ones.

It also takes Omit<ComponentPropsWithRef<"div">, "children">.

DataTable.Amount

PropTypeDefaultDescription
value

required

numberThe amount.
currency

required

stringIts 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

PropTypeDefaultDescription
value

required

stringThe identifier, such as an order's.

It also takes Omit<ComponentPropsWithRef<"span">, "children">.

DataTable.Link

PropTypeDefaultDescription
asChildbooleanRender 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`.

FieldTypeDefaultDescription
id

required

stringNames the column for sorting and keys.
header

required

stringThe header's text.
cell

required

(row: Row) => ReactNodeWhat 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".
grownumber1Its 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.
sortablebooleanWhether 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.

FieldTypeDefaultDescription
column

required

stringThe id of the column that sorts the table.
direction

required

"ascending" | "descending"Which way: "ascending" or "descending".

DataTablePagination

Where the rows shown sit among all of them, for the pagination under the table.

FieldTypeDefaultDescription
page

required

numberThe page shown, from 1.
pageSize

required

numberHow many rows a page holds.
total

required

numberHow many rows there are in all.
onPageChange

required

(page: number) => voidCalled with the page Previous or Next goes to.

DataTableLabels

The words a `DataTable` uses, for other languages.

FieldTypeDefaultDescription
selectAll

required

stringNames the header's checkbox, which selects the page.
selectRow

required

(row: string) => stringNames a row's checkbox, from the row's label.
actions

required

stringNames the actions column for screen readers.
actionsFor

required

(row: string) => stringNames a row's "…" button, from the row's label.
previous

required

stringThe button that goes back a page.
next

required

stringThe button that goes on a page.
range

required

(first: number, last: number, total: number) => stringWhich rows show, such as "1–14 of 42".
noResults

required

stringThe title of the row shown when filters leave nothing.
noResultsBody

required

stringThe line under it.
clearAll

required

stringThe button that clears the filters.
loading

required

stringWhat 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.

KeysWhat they do
Tab or Shift+TabMoves through its checkboxes, sortable headers, links, row actions and pagination, and onto its scroll area while it overflows
SpaceTicks or clears the focused checkbox; the header's selects the whole page
Enter or SpaceSorts by the focused header (ascending, then descending, then none), or opens a row's actions
Arrow keysScroll 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.

See it in the roastery demo's customers, orders, payouts, subscriptions and wholesale.