1. Documentation
  2. Data display
  3. Data Table
ReadmenpmGitHub
  • Introduction
  • Installation
  • Theming
  • Icons
  • Accordion
  • Card
  • Scroll Area
  • Separator
  • Sidebar
  • Field
  • Label
  • Input
  • Textarea
  • Number Field
  • OTP Field
  • Select
  • Combobox
  • Checkbox
  • Radio
  • Switch
  • Slider
  • Toggle
  • Calendar
  • Date Picker
  • File Upload
  • Button
  • Copy Button
  • Dropdown
  • Command
  • Tabs
  • Breadcrumb
  • Pagination
  • Tree
  • Table
  • Data Table
  • Stat
  • Timeline
  • Avatar
  • Status Bar
  • Badge
  • Meter
  • Empty
  • Skeleton
  • Spinner
  • Toast
  • Dialog
  • Sheet
  • Popover
  • Tooltip
  • Text
  • Kbd
  • Code Block
  • Utilities
  • Design rules

Data Table

Table with search, sorting, column visibility, selection, loading skeletons and two empty states.

Loading documentation…

Table< PreviousStatNext >

Powered by heyo

On this page

UsageColumnsLoading and emptyAgainst a serverProperties
WorkersEverything deployed in this project.
RegionStatus
acme-apifra1142,318Healthy
acme-webwaw18,904Healthy
acme-croniad1212Degraded
<DataTabletitle="Workers"description="Everything deployed in this project."searchableselectablerows={workers}getRowId={(worker) => worker.id}columns={[  { id: "name", header: "Worker", primary: true, sortBy: (w) => w.name, cell: (w) => w.name },  { id: "region", header: "Region", cell: (w) => w.region },  { id: "requests", header: "Requests", numeric: true, sortBy: (w) => w.requests, cell: (w) => w.requests.toLocaleString() },  { id: "status", header: "Status", cell: (w) => <Badge dot variant={w.tone}>{w.status}</Badge> },]}bulkActions={(selected, clear) => (  <Button size="sm" variant="destructive-secondary" onClick={() => remove(selected, clear)}>    Delete {selected.length}  </Button>)}/>

DataTable is Table with everything a real list needs bolted on: search, sorting, column visibility, selection with a bulk-action bar, loading skeletons and two different empty states.

Two layout decisions are worth knowing about.

  • The bulk bar replaces the toolbar in place. It does not appear above it: pushing the table down the moment you tick a checkbox moves the rows out from under the cursor, which is how you end up selecting the wrong one.
  • The whole thing is one card. Toolbar, table and footer share a single ring and one background, because they are one control — three stacked boxes read as three unrelated widgets.

Filtering and sorting are client-side by default: the 95% case is a page of a few hundred rows you already have, and a server round trip to filter those turns a snappy list into a spinner. Every input is also controllable, so the same component works against a server when the list outgrows the page.

Usage

tsx
import { DataTable } from "@heyo-sh/heyo-ui";<DataTable  rows={workers}  getRowId={(worker) => worker.id}  columns={[    { id: "name", header: "Worker", primary: true, cell: (w) => w.name },    { id: "region", header: "Region", cell: (w) => w.region },  ]}/>;

Columns

A column is data plus a renderer. cell returns a node, so formatting lives in the column and not in the rows.

tsx
const columns: DataTableColumn<Worker>[] = [  {    id: "name",    header: "Worker",    primary: true,    sortBy: (w) => w.name,    cell: (w) => w.name,  },  {    id: "requests",    header: "Requests",    numeric: true,    sortBy: (w) => w.requests,    cell: (w) => w.requests.toLocaleString(),  },  {    id: "actions",    header: "",    actions: true,    cell: (w) => <RowMenu worker={w} />,  },];
primarybooleanoptional

The column you scan for. Full contrast, medium weight.

numericbooleanoptional

Tabular figures, right-aligned.

sortBy((row) => string | number) | trueoptional

Makes the header a sort button. Return a comparable value, or sort the rows yourself and pass true to get just the affordance.

actionsbooleanoptional

Renders in the trailing actions column, revealed on hover.

fixed / hiddenbooleanoptional

fixed keeps a column out of the visibility menu; hidden starts it hidden but still listed.

Loading and empty

Three states, and they are not the same thing. loading draws skeleton rows, empty is "there is nothing here yet", and emptyFiltered is "your query matched nothing" — a different problem that deserves different words.

WorkerRegion
WorkerRegion

No workers yet

Deploy one and it will show up here.

<DataTable loading loadingRows={3} rows={[]} columns={columns} getRowId={byId} /><DataTablerows={[]}columns={columns}getRowId={byId}empty={<Empty title="No workers yet" description="Deploy one to see it here." />}/>

Against a server

Control the query and take over filtering. toolbar is the slot on the right of the search field — filters, a date range, a "New" button — and footer is the strip under the table, which is where Pagination belongs.

tsx
<DataTable  rows={page.rows}  columns={columns}  getRowId={(row) => row.id}  searchable  query={query}  onQueryChange={setQuery}  loading={isPending}  toolbar={<Button icon={IconPlus}>New worker</Button>}  footer={    <Pagination page={page.number} pageCount={page.count} onPageChange={goTo} />  }/>

Properties

rowsRow[]required

The data.

columnsDataTableColumn<Row>[]required

The column definitions.

getRowId(row: Row) => stringrequired

A stable id per row — selection and React keys both use it.

searchablebooleanoptional

Adds the search box. Matched against searchFields, or every cell.

searchFields(row: Row) => (string | number | null | undefined)[]optional

The values to match the query against. It defaults to JSON.stringify(row).

query / onQueryChangestring, fnoptional

Controlled query — pair it with server-side filtering.

selectablebooleanoptional

Adds the checkbox column and the bulk bar.

bulkActions(selected: string[], clear: () => void) => ReactNodeoptional

Rendered in the bar that replaces the toolbar once something is selected.

columnTogglebooleanoptional

The column visibility menu. It defaults to true whenever any column is toggleable.

toolbarReactNodeoptional

The toolbar slot on the right.

title / descriptionReactNodeoptional

A heading above the toolbar.

loading / loadingRowsboolean, numberoptional

Skeleton rows instead of data.

empty / emptyFilteredReactNodeoptional

Shown when there are no rows at all, and when the query matched nothing.

onRowClick(row: Row) => voidoptional

Makes rows interactive.

striped / stickyHeader / maxHeight—optional

Passed through to Table.

footerReactNodeoptional

The strip under the table — pagination, counts.