Skip to content
Design System
Contact

Table

Sortable, selectable records that follow the density setting.

Components<table>Checkbox

Playground

Click a header to sort (ascending, descending, then back to unsorted). Rows follow the row height token, so density changes every row at once. Scroll sideways on a phone: the company column stays pinned.

Preview
Demo data

Acme CorporationINV-2041MCMaya ChenPaidSep 26, 2026$18,400
NorthwindINV-2040JBJonas BergPendingSep 24, 2026$7,250
GlobexINV-2039PNPriya NairOverdueAug 28, 2026$32,900
InitechINV-2038TOTom OkaforPendingSep 22, 2026$4,120
HooliINV-2037LFLena FischerPaidSep 18, 2026$56,000
SoylentINV-2036DADiego AlvarezDraftSep 29, 2026$2,980
Vandelay IndustriesINV-2035MCMaya ChenOverdueAug 31, 2026$11,340
ContosoINV-2034PNPriya NairPaidSep 12, 2026$9,860
FabrikamINV-2033JBJonas BergPendingSep 15, 2026$15,500
Tailspin ToysINV-2032TOTom OkaforPaidSep 8, 2026$6,430

Props

Density
example.tsxtsx
1import { DataTable, type Column } from "@/components/design-system/components/table/data-table"
2
3const columns: Column<Invoice>[] = [
4 { id: "company", header: "Company", cell: (r) => r.company, sortValue: (r) => r.company },
5 { id: "status", header: "Status", cell: (r) => <StatusTag status={r.status} />, sortValue: (r) => order[r.status] },
6 { id: "amount", header: "Amount", align: "right", cell: (r) => formatCurrency(r.amount), sortValue: (r) => r.amount },
7]
8
9<DataTable
10 label="Invoices"
11 columns={columns}
12 rows={invoices}
13 getRowId={(r) => r.id}
14 selectable
15 stickyHeader
16/>

States

Row states at rest. Selection and status tints are opaque colour mixes, so pinned cells never show content scrolling underneath.

States
  • NorthwindPending$7,250
    GlobexPending$32,900
    Defaultrest, data-selected
  • NorthwindPending$7,250
    Hovertr:hover
  • NorthwindPending$7,250
    Focuscheckbox :focus-visible
  • Loadingaria-busy
  • SoylentArchived$2,980
    Disabledaria-disabled
  • Initech$4,120
    Errordata-status=error
  • Acme Corporation Saved$18,400
    Successdata-status=saved

Examples

A working invoices view. Search or filter until nothing matches to see the empty state. Select rows and mark them as paid: Initech fails once so the error and retry path is visible. Delete offers undo.

Invoices
Demo data

Acme CorporationINV-2041MCMaya ChenPaidSep 26, 2026$18,400
NorthwindINV-2040JBJonas BergPendingSep 24, 2026$7,250
GlobexINV-2039PNPriya NairOverdueAug 28, 2026$32,900
InitechINV-2038TOTom OkaforPendingSep 22, 2026$4,120
HooliINV-2037LFLena FischerPaidSep 18, 2026$56,000
SoylentINV-2036DADiego AlvarezDraftSep 29, 2026$2,980
Vandelay IndustriesINV-2035MCMaya ChenOverdueAug 31, 2026$11,340
ContosoINV-2034PNPriya NairPaidSep 12, 2026$9,860
FabrikamINV-2033JBJonas BergPendingSep 15, 2026$15,500
Tailspin ToysINV-2032TOTom OkaforPaidSep 8, 2026$6,430

10 of 10 invoicesTotal $164,780

API

PropTypeDefaultDescription
labelstring—Accessible name for the table, e.g. "Invoices".
columnsColumn<T>[]—Column definitions. A column with sortValue gets a sort button in its header.
rowsT[]—Rows to render after your own filtering. Sorting happens inside the table.
getRowId(row: T) => string—Stable id used for keys and selection.
density"default" | "compact""default"Sets data-density on the wrapper, which switches --row-h from 40px to 32px.
stripedbooleanfalseTints even rows with bg-subtle.
selectablebooleanfalseAdds a checkbox column, a tri-state header checkbox and the bulk action bar.
borderedbooleanfalseAdds vertical cell borders for dense numeric data.
stickyHeaderbooleanfalseCaps the height at 320px and pins the header row while the body scrolls.
loadingbooleanfalseRenders skeleton rows and sets aria-busy on the table.
getRowStatus(row: T) => "saving" | "saved" | "error" | "disabled"—Per-row status: busy, just-saved flash, failed or not selectable.
bulkActions(ids: string[], clear: () => void) => ReactNode—Actions shown in the selection bar for the selected rows.
emptyReactNode—Shown under the header when rows is empty, usually an EmptyState with a clear action.
Column.align"left" | "right""left"Right-align numbers and money; they also get tabular numerals.

Accessibility

Keyboard

Move between sort buttons, checkboxes and row actions
Tab
Sort by the focused column
EnterorSpace
Toggle the focused checkbox
Space
Move back
Shift+Tab

Semantics and ARIA

  • Native <table> markup with scope="col" headers, so screen readers announce the column for every cell.
  • Sortable headers carry aria-sort (ascending, descending or none) and contain a real button named after the column.
  • The header checkbox reports aria-checked="mixed" when some rows are selected and shows a minus glyph, not a tick.
  • Selection changes are announced through a polite live region, and the bulk bar is a labelled region.
  • Skeleton rows set aria-busy on the table; a saving row sets it on the row. The retry button names the invoice it retries.
  • Money is right-aligned with tabular numerals, so digits line up and columns can be scanned.