Morpha UI

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.

A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank Transfer$350.00
INV004PaidCredit Card$450.00
INV005PaidPayPal$550.00
Total$1,750.00
$ npx morpha-ui add table

Data 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-table

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

StatusEmailAmount
successken99@example.com316
successabe45@example.com242
processingmonserrat44@example.com837
successsilas22@example.com874
failedcarmella@example.com721

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.

Email
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".

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

EmailStatus
Amount
ken99@example.comsuccess
$316.00
abe45@example.comsuccess
$242.00
monserrat44@example.comprocessing
$837.00
silas22@example.comsuccess
$874.00
carmella@example.comfailed
$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.

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

EmailStatus
Amount
ken99@example.comsuccess
$316.00
abe45@example.comsuccess
$242.00
monserrat44@example.comprocessing
$837.00
silas22@example.comsuccess
$874.00
carmella@example.comfailed
$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.

Email
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
successken99@example.com
$316.00
successabe45@example.com
$242.00
processingmonserrat44@example.com
$837.00
successsilas22@example.com
$874.00
failedcarmella@example.com
$721.00

0 of 12 row(s) selected.

Rows per page
Page 1 of 3

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 table

API 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 TableCaption to name the table; it stays visible under the surface.
  • Action-only columns should carry an sr-only header label, and each row's trigger needs an aria-label that names the row it acts on.
  • Selection checkboxes need an aria-label too (Select all rows, Select ken99@example.com) — the checkbox has no visible text.

On this page