Navigation and layout

Data Table

The stateful table — a checkbox column, a selection action row, three-step column sorting, and a per-row actions menu.

⠋

Install

npx shadcn@latest add https://4af.selfsimilar.dev/r/data-table.json

Usage

import {
  DataTable,
  DataTableHead,
  DataTableRow,
  DataTableRowActions,
  TableBody,
  TableCell,
  TableHeader,
} from "@/components/ui/data-table";

<DataTable
  selectable
  selectionBar
  actionColumn
  rows={people.map((person) => person.id)}
  value={selected}
  onValueChange={setSelected}
  sort={sort}
  onSortChange={setSort}
  selectionActions={<Button size="xs" variant="ghost">Export</Button>}
>
  <TableHeader>
    <DataTableRow>
      <DataTableHead sortKey="name">Name</DataTableHead>
      <DataTableHead>Role</DataTableHead>
    </DataTableRow>
  </TableHeader>
  <TableBody>
    {sortPeople(people, sort).map((person) => (
      <DataTableRow
        key={person.id}
        value={person.id}
        selectLabel={`Select ${person.name}`}
        actions={
          <DataTableRowActions label={`Actions for ${person.name}`}>
            <DropdownMenuItem>Edit</DropdownMenuItem>
          </DataTableRowActions>
        }
      >
        <TableCell>{person.name}</TableCell>
        <TableCell>{person.role}</TableCell>
      </DataTableRow>
    ))}
  </TableBody>
</DataTable>

API

Main exports: DataTable, DataTableRow, DataTableHead, DataTableRowActions, TableSort, TableSortDirection

TableCaption, TableHeader, TableBody, TableFooter and TableCell are re-exported from table unchanged, so a data table is one import. DataTableRow replaces TableRow and DataTableHead replaces TableHead.

Use DataTableHead for every header cell, sortable or not — a plain TableHead won't fade under the selection bar.

Props

DataTable

Takes every Table prop and adds:

PropTypeDefaultDescription
selectablebooleanfalseAdds the checkbox column on the left.
rowsreadonly string[][]Row values the select-all covers.
valuereadonly string[]—Selected row values, controlled.
defaultValuereadonly string[][]Selected row values, uncontrolled.
onValueChange(value: string[]) => void—Fired with the next selection.
selectAllLabelstring"Select all rows"Names the select-all checkbox.
selectionBarbooleanfalseTurn the header into an action row while anything is selected.
selectionActionsReactNode—Buttons for the right end of that row.
actionColumnbooleanfalseAdds the per-row actions column on the right.
actionColumnLabelstring"Actions"Names that column for assistive technology.
sortTableSort | null—Active column and direction, controlled.
defaultSortTableSort | nullnullActive column and direction, uncontrolled.
onSortChange(sort: TableSort | null) => void—Fired with the next step of the cycle.

DataTableRow

PropTypeDescription
valuestringMakes the row selectable and names it in the selection.
selectLabelstringNames this row's checkbox. Defaults to "Select row".
actionsReactNodeWhat goes in the actions column. Needs actionColumn.

DataTableHead

PropTypeDescription
sortKeystringMakes the column sortable and names it in TableSort.column.

TableSort is { column: string; direction: "asc" \| "desc" }.

DataTableRowActions

Takes every DropdownMenuContent prop — side, align, sideOffset — and adds:

PropTypeDefaultDescription
labelstring—Required. Names this row's menu.
iconReactNode<Ellipsis />The trigger's glyph.
childrenReactNode—The menu's items.

Columns

selectable and actionColumn go on the root, not on individual rows: the header and the placeholder rows have to grow the column too. Never write a gutter cell into a row by hand.

Selection

A row is selectable when it has a value; one without still gets an empty cell, so a "no results" row keeps its columns aligned.

rows sets what select-all covers — pass the page being rendered, not every row held. Selecting all is a union, so selected values outside rows are kept. The header checkbox is indeterminate while the selection is partial and checks from there.

Rows expose data-selected for styling; their checkboxes expose selection to assistive technology.

selectionBar turns the header into an action row while anything is selected: the labels give way to a count and selectionActions sits at the right end, with the select-all still in place so the selection can be cleared. The count covers the whole selection, not only the part rows covers. Use size="xs" for the actions.

<DataTable
  selectable
  selectionBar
  selectionActions={<Button size="xs" variant="ghost">Export</Button>}
>

Row actions

Each row's actions goes in the actions column. DataTableRowActions is the menu that normally belongs there — it owns the trigger and you own the items:

<DataTableRow
  value={release.id}
  actions={
    <DataTableRowActions label={`Actions for ${release.version}`}>
      <DropdownMenuItem>Pin release</DropdownMenuItem>
      <DropdownMenuItem variant="destructive">Yank</DropdownMenuItem>
    </DataTableRowActions>
  }
>

label is required: name each menu for its row, not "More actions" repeated down a column. The trigger's size follows the table's density.

actions is a ReactNode, so a row can pass plain buttons, a link, or its own DropdownMenu instead.

The contents are hidden until the row is hovered, and also shown on visible focus within the row, while the menu is open, and wherever the pointer has no hover. Selection does not reveal them. Clicks inside the cell don't bubble to the row.

With bordered, neither gutter column draws a vertical rule beside it.

Sorting

sortKey cycles ascending, descending, unsorted; switching columns restarts at ascending. aria-sort is set on the <th>. An unsorted sortable header shows ChevronsUpDown.

The component owns the sort control and its state; ordering the rows is up to the caller:

const [sort, setSort] = useState<TableSort | null>(null);
const rows = useMemo(() => sortPeople(people, sort), [people, sort]);

<DataTable sort={sort} onSortChange={setSort}>
  <DataTableHead sortKey="name">Name</DataTableHead>

Loading

As on the base table, plus: placeholder rows grow the gutter cells and stay in column, and the select-all and sort controls are disabled.

<DataTable selectable actionColumn loading loadingRows={8}>