Skip to content
emstein ui

Table

A virtualized data table with sorting, resizing, row selection, and growth-based column widths, plus the familiar shadcn table primitives.

Installation

npx shadcn@latest add @emstein-ui/table

Or install straight from the registry URL: npx shadcn@latest add https://ui.emstein.com/r/table.json

Usage

import { Table, type EmsteinUITableColumn } from "@/components/emstein-ui/table"
type User = {
  id: string
  name: string
  email: string
  score: number
}

const columns: EmsteinUITableColumn<User>[] = [
  { key: "name", label: "Name", type: "text" },
  { key: "email", label: "Email", type: "longText" },
  { key: "score", label: "Score", type: "number", align: "right" },
]

export function UsersTable({ users }: { users: User[] }) {
  return <Table data={users} columns={columns} getRowId={(user) => user.id} />
}

Pass data and columns; virtualization, sorting, and resizing work out of the box. Each column's type picks a sensible width, and every preset can be overridden. See column widths.

Sorting and resizing

Click anywhere in a sortable header cell to switch between ascending and descending order. The first sortable column starts sorted ascending; use initialSort to pick another column, or initialSort={false} to start unsorted.

Hover a header to reveal its resize divider, then drag. A column trades width with its neighbor, so the table keeps its overall width while you resize. Turn either feature off for the whole table with sortable and resizable, or per column with the same keys.

Scroll modes

By default, rows are virtualized against the window: the table grows with its content and the page scrolls. Set scrollMode="table" to render inside a fixed-height scroll container instead, as in the preview above.

<Table
  data={rows}
  columns={columns}
  scrollMode="table"
  tableHeight={480}
/>

Examples

Row selection

Pass selection to add a checkbox column. Control it with selectedRows and onSelectedRowsChange, or pass defaultSelectedRows and let the table manage it. Ids come from getRowId.

Task
Title
Assignee
Priority
Estimate
TASK-1280Document the public APITheo WellsLow6h
TASK-5562Add CSV export to reportsEli HartMedium5h
TASK-7839Refresh pricing page copyLeah LevinMedium2h
TASK-7878Migrate billing webhooksNoah StoneHigh8h
TASK-8686Fix timezone drift in schedulesIris LaneHigh4h
TASK-8782Audit onboarding emailsMaya ReedHigh3h

1 of 6 row(s) selected.

Set footer on a column to render a summary row. Pass a value, or a function that receives the full data and the currently rendered rows.

Invoice
Customer
Method
Status
Amount
INV-001Northwind TradersCardPaid$250.00
INV-002Globex CorporationTransferPending$150.00
INV-003InitechCardPaid$350.00
INV-004Umbrella HealthInvoiceOverdue$450.00
INV-005Stark IndustriesCardPaid$550.00
Total$1,750.00

Primitives

Render Table without data and columns and you get a plain table with the same primitives as shadcn/ui, so existing table markup keeps working after you switch.

A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank Transfer$350.00
INV004PaidCredit Card$450.00
Total$1,200.00

Column widths

Every column type sets a starting width, a minimum width, and a grow factor. When the container is wider than the columns, the extra space is shared in proportion to grow. When it is narrower, columns keep their minimum widths and the table scrolls horizontally.

TypeWidthMin widthGrow
text180px120px1
longText280px180px2
shortText140px96px0.5
number104px80px0
currency124px96px0
date132px112px0
status128px104px0
actions72px56px0

Override any preset per column with width, minWidth, and grow.

const columns: EmsteinUITableColumn<Payout>[] = [
  { key: "name", label: "Name", type: "text" },
  // Takes three shares of any extra space.
  { key: "description", label: "Description", type: "longText", grow: 3 },
  // Fixed at 140px: never grows, still resizable.
  { key: "net", label: "Net", type: "currency", align: "right", width: 140, grow: false },
]

API Reference

Table

PropTypeDefault
datarequired

Rows to render.

TData[]—
columnsrequired

Column definitions. See Column below.

EmsteinUITableColumn<TData>[]—
getRowId

Returns a stable id for each row. Selection state is keyed by this id.

(row: TData, index: number) => stringrow index
selection

Adds a checkbox column. See Selection below.

EmsteinUITableSelection—
sortable

Enables sorting for every column that doesn't opt out.

booleantrue
initialSort

The starting sort. Defaults to the first sortable column, ascending. Pass false to start unsorted.

{ key: string; direction?: "asc" | "desc" } | false—
showUnsortedSortIcon

Shows a muted chevron on sortable columns that aren't sorted.

booleanfalse
resizable

Enables resizing by dragging the edge of a header cell.

booleantrue
scrollMode

Virtualize rows against the window, or inside a fixed-height scroll container.

"page" | "table""page"
tableHeight

Height of the scroll container when scrollMode is "table".

number560
rowHeight

Row height in pixels, used for virtualization.

number56
emptyText

Shown when there are no rows.

string"No results."
caption

Optional caption for the table.

ReactNode—
className

Classes for the outer container. containerClassName is an alias.

string—
tableClassName

Also available: headerClassName, bodyClassName, footerClassName, and captionClassName.

string—

Column

Columns are typed as EmsteinUITableColumn<TData>.

PropTypeDefault
keyrequired

Property to read from each row. Also used as the column id.

keyof TData | string—
labelrequired

Header text.

string—
type

Width preset. See Column widths.

"text" | "longText" | "shortText" | "number" | "currency" | "date" | "status" | "actions""text"
render

Custom cell content. Sorting still uses the raw value.

(row: TData) => ReactNode—
align

Alignment for the header, cells, and footer.

"left" | "center" | "right""left"
width

Starting width in pixels.

numberpreset
minWidth

Minimum width in pixels.

numberpreset
grow

Share of extra container width. true is 1, false is 0.

boolean | numberpreset
sortable

Set to false to disable sorting for this column.

booleantrue
resizable

Set to false to disable resizing for this column.

booleantrue
footer

Footer content. Functions receive all data and the currently rendered rows.

ReactNode | ({ data, rows }) => ReactNode—
className

Classes for this column's body cells.

string—
footerClassName

Classes for this column's footer cell.

string—

Selection

The selection prop is typed as EmsteinUITableSelection. Pass an empty object for uncontrolled selection with nothing selected.

PropTypeDefault
selectedRows

Selected row ids. Makes selection controlled.

string[]—
onSelectedRowsChange

Called with the new list of selected ids.

(rows: string[]) => void—
defaultSelectedRows

Initially selected ids when selection is uncontrolled.

string[][]