Table
A responsive table for displaying tabular data, and the base for building data tables.
Overview
Table is a thin, styled layer over the native table elements: the container is
the surface (a surface-container-lowest card hugged by an outline-variant
hairline) and owns the horizontal scroll, so a wide table never pushes the page.
Headers sit on type-label in on-surface-variant, cells on type-body in
on-surface, and rows tint on hover — gated to devices that can actually hover —
with a primary-container wash for the selected state.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| INV004 | Paid | Credit Card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| Total | $1,750.00 | ||
$ npx morpha-ui add tableData Table
The Table component is intentionally unopinionated: it renders markup, it does
not manage state. For sorting, filtering, pagination or selection, pair it with
TanStack Table, which is headless and
leaves the rendering to the kit.
npm install @tanstack/react-tableThe examples below all use the same Payment shape:
export type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}Basic
Define your columns with ColumnDef, build the instance with useReactTable +
getCoreRowModel, and render the header groups and rows with flexRender.
| Status | Amount | |
|---|---|---|
| success | ken99@example.com | 316 |
| success | abe45@example.com | 242 |
| processing | monserrat44@example.com | 837 |
| success | silas22@example.com | 874 |
| failed | carmella@example.com | 721 |
Cell formatting
Cells are just React — format the value and style the wrapper. Money is
right-aligned on both the header and the cell (so the column reads as one rail)
and set in tabular-nums, so digits line up across rows.
Amount | |
|---|---|
| ken99@example.com | $316.00 |
| abe45@example.com | $242.00 |
| monserrat44@example.com | $837.00 |
| silas22@example.com | $874.00 |
| carmella@example.com | $721.00 |
Row actions
Add a trailing column with an IconButton trigger opening the kit's DropdownMenu.
The row's data is available as row.original, and the destructive entry uses
DropdownMenuItem variant="destructive".
Amount | Actions | |
|---|---|---|
| ken99@example.com | $316.00 | |
| abe45@example.com | $242.00 | |
| monserrat44@example.com | $837.00 | |
| silas22@example.com | $874.00 | |
| carmella@example.com | $721.00 |
Pagination
Pass getPaginationRowModel and drive the page with table.previousPage() /
table.nextPage(); getCanPreviousPage() and getCanNextPage() disable the
buttons at the ends.
| Status | Amount | |
|---|---|---|
| ken99@example.com | success | $316.00 |
| abe45@example.com | success | $242.00 |
| monserrat44@example.com | processing | $837.00 |
| silas22@example.com | success | $874.00 |
| carmella@example.com | failed | $721.00 |
Page 1 of 3
Sorting
Hold SortingState, pass getSortedRowModel, and turn the header into a text
Button that calls column.toggleSorting() — the arrow glyph follows
column.getIsSorted().
| ken99@example.com | $316.00 |
| abe45@example.com | $242.00 |
| monserrat44@example.com | $837.00 |
| silas22@example.com | $874.00 |
| carmella@example.com | $721.00 |
| jason@example.com | $125.00 |
Filtering
Hold ColumnFiltersState, pass getFilteredRowModel, and wire an InputGroup
to table.getColumn("email")?.setFilterValue(). When nothing matches, render an
empty row spanning every column instead of a blank body.
Amount | |
|---|---|
| ken99@example.com | $316.00 |
| abe45@example.com | $242.00 |
| monserrat44@example.com | $837.00 |
| silas22@example.com | $874.00 |
| carmella@example.com | $721.00 |
| jason@example.com | $125.00 |
| noah@example.com | $452.00 |
| mia@example.com | $68.00 |
| liam@example.com | $990.00 |
| emma@example.com | $233.00 |
| oliver@example.com | $517.00 |
| ava@example.com | $145.00 |
Column visibility
Hold VisibilityState and let a DropdownMenu of DropdownMenuCheckboxItems call
column.toggleVisibility(). Columns opt out of hiding with enableHiding: false.
| Status | Amount | |
|---|---|---|
| ken99@example.com | success | $316.00 |
| abe45@example.com | success | $242.00 |
| monserrat44@example.com | processing | $837.00 |
| silas22@example.com | success | $874.00 |
| carmella@example.com | failed | $721.00 |
Row selection
Add a leading select column with the kit's Checkbox. The header checkbox
takes the real mixed state — indeterminate is a separate prop, not a fake
glyph — and the selected row is marked with data-state="selected", which the
TableRow styles as a primary-container wash.
Amount | ||
|---|---|---|
| ken99@example.com | $316.00 | |
| abe45@example.com | $242.00 | |
| monserrat44@example.com | $837.00 | |
| silas22@example.com | $874.00 | |
| carmella@example.com | $721.00 |
0 of 5 row(s) selected.
Reusable components
Everything above, composed: a filter + column-toggle toolbar, a ColumnHeader
that folds sorting and hiding into one menu, row selection, row actions, and a
Pagination bar with a page-size Select and first/prev/next/last controls.
Lift these three pieces (ColumnHeader, ColumnToggle, Pagination) out once
and every data table in your app can reuse them.
| Actions | ||||
|---|---|---|---|---|
| success | ken99@example.com | $316.00 | ||
| success | abe45@example.com | $242.00 | ||
| processing | monserrat44@example.com | $837.00 | ||
| success | silas22@example.com | $874.00 | ||
| failed | carmella@example.com | $721.00 |
0 of 12 row(s) selected.
Installation
Morpha UI components are distributed via the Morpha CLI: npx morpha-ui add table. In the meantime, since this is the base shadcn component, you can also
add it directly:
npx shadcn@latest add tableAPI Reference
Prop
Type
The other parts (TableHeader, TableBody, TableFooter, TableRow,
TableHead, TableCell, TableCaption) take the props of the native element
they render — thead, tbody, tfoot, tr, th, td, caption.
Accessibility
- Renders real table semantics (
table/thead/tbody/th/td), so screen readers announce row and column context. - Use
TableCaptionto name the table; it stays visible under the surface. - Action-only columns should carry an
sr-onlyheader label, and each row's trigger needs anaria-labelthat names the row it acts on. - Selection checkboxes need an
aria-labeltoo (Select all rows,Select ken99@example.com) — the checkbox has no visible text.