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/tableOr 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-1280 | Document the public API | Theo Wells | Low | 6h | |
| TASK-5562 | Add CSV export to reports | Eli Hart | Medium | 5h | |
| TASK-7839 | Refresh pricing page copy | Leah Levin | Medium | 2h | |
| TASK-7878 | Migrate billing webhooks | Noah Stone | High | 8h | |
| TASK-8686 | Fix timezone drift in schedules | Iris Lane | High | 4h | |
| TASK-8782 | Audit onboarding emails | Maya Reed | High | 3h |
1 of 6 row(s) selected.
Footer
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-001 | Northwind Traders | Card | Paid | $250.00 |
| INV-002 | Globex Corporation | Transfer | Pending | $150.00 |
| INV-003 | Initech | Card | Paid | $350.00 |
| INV-004 | Umbrella Health | Invoice | Overdue | $450.00 |
| INV-005 | Stark Industries | Card | Paid | $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.
| 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 |
| 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.
| Type | Width | Min width | Grow |
|---|---|---|---|
| text | 180px | 120px | 1 |
| longText | 280px | 180px | 2 |
| shortText | 140px | 96px | 0.5 |
| number | 104px | 80px | 0 |
| currency | 124px | 96px | 0 |
| date | 132px | 112px | 0 |
| status | 128px | 104px | 0 |
| actions | 72px | 56px | 0 |
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
| Prop | Type | Default |
|---|---|---|
datarequiredRows to render. | TData[] | — |
columnsrequiredColumn definitions. See Column below. | EmsteinUITableColumn<TData>[] | — |
getRowIdReturns a stable id for each row. Selection state is keyed by this id. | (row: TData, index: number) => string | row index |
selectionAdds a checkbox column. See Selection below. | EmsteinUITableSelection | — |
sortableEnables sorting for every column that doesn't opt out. | boolean | true |
initialSortThe starting sort. Defaults to the first sortable column, ascending. Pass | { key: string; direction?: "asc" | "desc" } | false | — |
showUnsortedSortIconShows a muted chevron on sortable columns that aren't sorted. | boolean | false |
resizableEnables resizing by dragging the edge of a header cell. | boolean | true |
scrollModeVirtualize rows against the window, or inside a fixed-height scroll container. | "page" | "table" | "page" |
tableHeightHeight of the scroll container when | number | 560 |
rowHeightRow height in pixels, used for virtualization. | number | 56 |
emptyTextShown when there are no rows. | string | "No results." |
captionOptional caption for the table. | ReactNode | — |
classNameClasses for the outer container. | string | — |
tableClassNameAlso available: | string | — |
Column
Columns are typed as EmsteinUITableColumn<TData>.
| Prop | Type | Default |
|---|---|---|
keyrequiredProperty to read from each row. Also used as the column id. | keyof TData | string | — |
labelrequiredHeader text. | string | — |
typeWidth preset. See Column widths. | "text" | "longText" | "shortText" | "number" | "currency" | "date" | "status" | "actions" | "text" |
renderCustom cell content. Sorting still uses the raw value. | (row: TData) => ReactNode | — |
alignAlignment for the header, cells, and footer. | "left" | "center" | "right" | "left" |
widthStarting width in pixels. | number | preset |
minWidthMinimum width in pixels. | number | preset |
growShare of extra container width. | boolean | number | preset |
sortableSet to false to disable sorting for this column. | boolean | true |
resizableSet to false to disable resizing for this column. | boolean | true |
footerFooter content. Functions receive all data and the currently rendered rows. | ReactNode | ({ data, rows }) => ReactNode | — |
classNameClasses for this column's body cells. | string | — |
footerClassNameClasses 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.
| Prop | Type | Default |
|---|---|---|
selectedRowsSelected row ids. Makes selection controlled. | string[] | — |
onSelectedRowsChangeCalled with the new list of selected ids. | (rows: string[]) => void | — |
defaultSelectedRowsInitially selected ids when selection is uncontrolled. | string[] | [] |