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.jsonUsage
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:
DataTableRow
DataTableHead
TableSort is { column: string; direction: "asc" \| "desc" }.
DataTableRowActions
Takes every DropdownMenuContent prop — side, align, sideOffset — and adds:
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}>