Data Table
ProA data grid built on TanStack Table for behavior (sorting, grouping, row selection, column pinning) with Koala styling on top. Drive features from column defs, or compose the styled primitives directly.
Installation#
Usage#
Koala splits the two concerns the way TanStack does. The Table primitives own the look; TanStack owns the behavior. DataTable wires them together: hand it columns and data, flip on the features you want, and ride alignment and pinning along on each column’s meta.
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
import {
DataTableTabs,
DataTableTabsList,
DataTableTab,
DataTableToolbar,
DataTableToolbarSection,
DataTableSearch,
DataTable,
Table,
TableCaption,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/data-table"
<DataTableTabs>
<DataTableTabsList>
<DataTableTab />
</DataTableTabsList>
<DataTableToolbar>
<DataTableToolbarSection>
<DataTableSearch />
</DataTableToolbarSection>
<DataTableToolbarSection />
</DataTableToolbar>
<DataTable />
</DataTableTabs>
<Table>
<TableCaption />
<TableHeader>
<TableRow>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell />
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell />
</TableRow>
</TableFooter>
</Table>Container#
The table is chromeless by default (no border, radius, or fill, with the edge cells pulled flush) so it reads as lined records and drops into whatever already frames it. That is the minimal surface you see throughout this page. When the table needs to stand on its own, set variant="container" to wrap it in the bordered, rounded card. Striping, hover, and pinned columns rebase onto each surface automatically, so every feature keeps working either way.
Left at its default, the table drops straight into the page section it belongs to (a settings panel, a dashboard block) sitting on the page surface with only its own row rules, no box stacked around content that’s already laid out. The minimal surface renders on the page background; for a distinct surface, reach for variant="container".
When the table is the whole pane (a split view, the body under a tab bar), variant="full-bleed" lets it fill the pane edge to edge: the row rules and the hover reach both sides, and the first and last cells take the pane’s gutter (--table-inset, 16px) instead of being pulled flush. Put it straight in the pane, with no padding around it, and let the pane’s own borders frame it, so no line is drawn twice.
Flush edges would leave no room for a control’s focus ring or its invisible hit area, so the minimal scroll box bleeds: it reaches a strip past the table on every side (at least 4px, a 2px ring on a 2px offset, even when an app shrinks the root font size) and pads it back, so the columns sit exactly where they would and the rings land inside the box. The same strip is its scroll padding, so tabbing to a row at the edge of a scrolled table stops short of the edge. Controls that hug a cell edge (the sort buttons, the expand toggles, the reorder grips) draw their ring inside themselves. A table wider than its box still scrolls: there, a control half under the edge is half scrolled out of view, ring and all, until you scroll to it.
Toolbar#
The control rail above the table is built in and toggled with simple booleans, no wiring. searchable adds a search box that filters every column (TanStack’s global filter), and toolbarActions is a right-side slot for your own buttons (filter, export, a primary action). The search and any icon buttons are Koala natives (Input and Button), so they share the system’s focus rings, density, and press feedback.
On a phone the rail reflows by itself: the search takes the first row at full width, and the filters and the right-hand actions share the next one, filters from the left and actions pushed to the right edge. A single view-options button never ends up alone on a row of its own, so there is nothing to override and no control to drop for small screens.
Need a layout the toggles don’t cover? The parts: DataTableToolbar, DataTableToolbarSection, and DataTableSearch are exported, so you can compose your own rail and drop it above a plain DataTable.
View options#
viewOptions adds a SlidersHorizontal dropdown to the toolbar for showing and hiding columns. Every column with a label appears as a checkbox, and the menu stays open so you can toggle several at once. Give a column a meta.label for its name there (a string header is used as the fallback); the selection and icon-only action columns have no label, so they’re left out.
enableCardLayout adds a Rows/Cards switch to the same dropdown and renders the rows as a responsive card grid when chosen: the first column becomes the card title, the rest stack as labelled fields, and an icon-only action rides along top-right. Pass renderCard to take over the card entirely. The cards cascade in on load via the shared Stagger primitive.
Filtering#
Beyond the global searchable box, pass filters to add per-column faceted filters. Each field becomes a multi-select dropdown in the toolbar (with the count of matching rows per option) and a removable chip below it; a Reset clears them all. Filtered columns use the built-in filterFn: "arrIncludesSome".
The parts are exported too: DataTableFacetedFilter and DataTableActiveFilters, for hand-composed toolbars over a table instance you own.
Tabs#
Line tabs over a table split one list into views (All, Active, Invited), each with its count. Wrap the table in DataTableTabs, put a DataTableTabsList of DataTableTabs above it, and hand each tab its count. The rows stay yours to filter, from the same place the counts come from; the parts own the rest. The table becomes the panel the tabs control, so a screen reader hears which view it is in, and switching views takes it back to its first page while the search, the sort and the selection that still applies stay put. No remounting the table with a key to reset the page, and nothing lost when you do.
Remembering view state#
Pass a persistKey and the table remembers how the user left it (which columns are visible, their order, the sort, the rows/cards layout, and the page size) by writing them to localStorage under that key. It’s SSR-safe: the server renders the defaults and the saved state is applied after mount, so hydration stays clean. Hide a column or switch to cards below, then reload the page, and it comes back the same.
Use a key that’s unique per table (and per user, if you scope storage that way). Persistence is best-effort: unavailable or full storage fails quietly and the table just opens at its defaults.
Cell types#
A column’s cell renders anything, so a row reads like a record rather than a wall of text. Compose the cells from the same DS parts you use everywhere else: an avatar beside two-line text (TableCellText), a status Badge, a row of tag chips, a directional trend delta tinted success/destructive, an inline sparkline built on Chart in sparkline mode, and an overlapping Avatar stack with a +N overflow. Numbers right-align with meta.numeric (it adds tabular-nums).
Column tooltips#
Two per-column hints ride along on meta, both backed by the shared Tooltip. headerTooltip appends a small info icon after the header label, for explaining what a column measures without crowding the title (hover the Balance header). cellTooltip is a function given the cell context that returns the hint to show for each cell. Return null to skip one. It pairs naturally with a clamped column: truncate the cell and reveal the full value on hover or focus (hover a member name). Both triggers are keyboard-reachable.
The cell hint fills the cell exactly as its content would, so it never changes the column’s layout: a full-width Progress keeps its width (hover an Onboarding bar), text keeps the column’s alignment, and a truncated cell still truncates.
Sorting#
Set enableSorting to make headers click-to-sort. The header shows a muted up/down caret until active, then a solid directional one. Seed an order with initialSorting.
Reordering#
Sorting computes an order; reordering lets the user author one. Pass onRowReorder and every row grows a grip: drag it and the move arrives as the two indices it travelled between, which you apply to your own data (moveItem does the array move). The table never reorders data itself, so the order stays in the source of truth you already own and is yours to save.
The row you grab is lifted, and the rows it passes step aside: the gap that opens is the drop indicator, so there is no line to read and no guessing which side of it you land on. Release and the row eases into the gap before the move is applied, so it settles rather than jumps. The gesture runs on pointer events, not the HTML5 drag-and-drop API, which is what lets it work on touch, keep its own cursor, and pull the view along when you drag past the edge of a scrolling table. Esc cancels mid-drag and leaves the data alone.
The grip is the only drag source, so selecting text in a cell still works, and it has a keyboard twin: focus it and press ↑ or ↓ to walk the row one place at a time, with each move announced to screen readers. A hand-made order only means something while the rows are in data order, so the grips go inert (and say why) under a sort or a grouping.
enableColumnOrdering does the same for columns, and a column travels whole: the header and every cell under it move together, with the columns on either side opening the gap. Grip it, or nudge it with ← / →. That one the table can own outright, so there is nothing to wire; take onColumnOrderChange if you want to know, seed it with initialColumnOrder, or hand the table a persistKey and it remembers the order itself. Columns pinned with meta.sticky stay where they are pinned.
Row hover#
Rows highlight on hover by default: only body rows, never the header. Pass hoverable={false} to opt out (the striped example below does, so the zebra pattern stays crisp).
Striped rows#
striped zebra-stripes even rows for faster scanning. The stripe is scoped to skip hovered and selected rows, so those states always read clearly.
Condensed rows#
Density is Koala’s cross-cutting spacing axis (see Density). For a table it’s the condensed-rows knob: compact is the dense default; comfortable is roomier. Set it per-table or for a whole subtree with DensityProvider.
Grouping#
enableGrouping with an initialGrouping column collapses rows into expandable groups. Give a column an aggregationFn and aggregatedCell to summarize each group; here the balance column sums per team.
Expandable rows#
Pass renderSubRow to reveal a detail panel under a row. It prepends a caret toggle column and renders your node full-width beneath the expanded row. Limit which rows can expand with getRowCanExpand.
With checkboxes#
enableRowSelection prepends a checkbox column with a header “select all” that goes indeterminate on a partial selection. Subscribe with onRowSelectionChange; pass a stable getRowId so selection survives re-sorts.
The selection control is the shared Checkbox component, in checked, indeterminate, and disabled states:
Bulk actions#
With selection on, pass renderSelectionActions and a floating pill rises while rows are selected: the live count, a clear button, and your actions. It’s a dark bar (the Figma/Linear look) that reads as one distinct object over the table, and any Koala Button or menu you drop in re-tints to it automatically. It overlays the bottom of the table out of flow, so it appears and leaves without shifting the toolbar above or anything on the page below. The renderer receives the selected rows and the table instance, so an action can read the selection and reset it. Select a couple of rows below.
Opening a row#
The record-list pattern: click a row and the screen shows that record, in a pane beside the table, a drawer or a page of its own. Pass onRowClick and every row opens on a click anywhere across it, while the controls inside keep their own jobs (the checkbox checks, the row menu opens, a link navigates) and a drag that selects text is left alone. Rows become focusable with a brand ring, and Enter on a focused row opens it, so the keyboard gets the same path as the pointer.
Tell the table which record is open with activeRowId. That row takes an ink tint, deliberately distinct from the checkbox-selected muted fill, since a row can be picked for a bulk action and open at once, and it carries aria-current. To walk the list from the pane, onDisplayedRowsChange hands you the rows in the order the table shows them, search and sort included, so previous and next follow the screen. Search, re-sort, then step through below.
Sticky header#
stickyHeader pins the header while the body scrolls. Cap the scroll container’s height with containerClassName (e.g. max-h-72) to create the scroll. The box keeps a scroll padding the height of the header, so a row that keyboard focus scrolls up to lands below the header, ring included, instead of under it.
Sticky column#
Pin a column to an edge with meta: { sticky: "left" } (or "right"). It stays put, tracking the row’s hover and selected background, while the rest scrolls sideways.
Pagination#
enablePagination pages the rows client-side and drops a Pagination toolbar below the table: prev/next, a “Page X of Y” readout, and a rows-per-page select. Set the initial page size with pageSize and the select’s choices with pageSizeOptions. It shares the table’s density, and pages are disabled while loading.
The page never points past the end. When data shrinks under it (a delete, a filter your screen applies itself) the table steps back to the last page that still exists, and an edit that keeps the row count keeps the page. The rows-per-page select always offers the size in use: left out of pageSizeOptions, it is merged in and sorted, and with no options at all they are built around pageSize (one, two and three pages, then 50 and 100). Go to page 3 below, then remove rows.
Load on scroll#
For server-driven lists, swap paging for infinite scroll: pass onLoadMore and a hasMore flag, and the table fetches the next batch as a sentinel scrolls into view (prefetching ~200px early). Flip loadingMore while the request is in flight; it shows a loading row and blocks duplicate calls. You fetch and append to data; the sentinel watches the nearest scroll container (here a capped max-h wrapper) or the viewport. It replaces the pagination toolbar; the two are mutually exclusive.
Server-side data#
By default the table sorts, filters, and pages the rows it’s given. For large or remote datasets, flip the work to the server: set manualSorting, manualFiltering, and/or manualPagination, then fetch on the onSortingChange / onPaginationChange / onColumnFiltersChange callbacks and feed back the current page as data. Pass pageCount (or rowCount) so the pager knows the total. The demo simulates a 500ms round-trip per sort and page.
Controlled state#
The table keeps its own state until you want it. Pass sorting, globalFilter or columnVisibility with the matching on*Change and that piece is yours: reset it from a button, keep it in the URL, or drive the search from a field elsewhere on the screen (a controlled globalFilter filters with or without the built-in box). Left uncontrolled, every on*Change still reports each change, the built-in search box included, and always after the table has committed it, never from inside a state update, so setting your own state straight from one is safe.
onDisplayedRowsChange reads the other way: it hands you the rows the table is showing, in its order, across every page, after mount and whenever that list changes. Use it for a summary, an export of the current view, or previous and next in a detail pane. Sort or search below and the line under the table follows.
Loading#
Pass loading to swap the rows for skeleton placeholders while data is in flight. They mirror the real column layout (numeric columns get a short right-aligned bar, the select column a checkbox-sized square) so the table doesn’t reflow when data lands. Tune the count with loadingRows; the region is announced with aria-busy.
AI generation#
This example takes the existing members table and adds an AI insight column. Click Generate with AI to fill it row by row; completed insights stay visible while the next one is generated. The preview uses local sample responses, ready to be replaced by a model call in your app.
Empty states#
When there are no rows, the table renders DataTableEmpty, a preset over EmptyState built for the two cases a grid hits. The default is kind="empty" (nothing here yet); override the emptyState prop and pass action buttons as children.
When a search or filter comes up empty, switch to kind="search": the not-found icon and copy, with a “clear filters” escape hatch. With the built-in searchable box or filters this happens automatically: an active search or filter that matches nothing falls back to the search-empty placeholder.
Primitives#
For a static or fully bespoke table, skip TanStack and compose the styled primitives directly: Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell. The same variants (variant, striped, hover, density, sticky) apply.
API reference#
DataTable
The TanStack-wired grid: it owns sorting, filtering, selection, grouping, paging and reordering, and renders into the Table primitives. Every part that renders an element accepts className, merged last; here it lands on the <table>.
| Prop | Type | Default | Description |
|---|---|---|---|
| columns* | ColumnDef<TData, TValue>[] | - | TanStack column definitions. Alignment, pinning, labels and tooltips ride on each column's meta (see ColumnMeta). |
| data* | TData[] | - | The rows. Keep the array and its items stable between renders (state or memo). |
| getRowId | (row: TData, index: number) => string | - | Stable row id, so selection, grouping and activeRowId survive re-sorts. Falls back to the row index. |
| variant | "minimal" | "container" | "full-bleed" | "minimal" | minimal is chromeless with flush edge cells; container is the ring-edged, rounded card; full-bleed fills its pane edge to edge, with the edge cells padded by --table-inset. |
| density | "comfortable" | "compact" | "compact" | Row height and cell padding; also sets the toolbar controls (sm or md) and the pager. Falls back to the nearest DensityProvider. |
| striped | boolean | false | Zebra-stripe even rows; the stripe skips hovered, selected and current rows. |
| hoverable | boolean | true | Tint body rows on hover. |
| stickyHeader | boolean | false | Pin the header while the body scrolls. Cap the height with containerClassName. |
| containerClassName | string | - | Classes for the scroll container, e.g. max-h-96 to give a sticky header something to scroll. |
| enableSorting | boolean | false | Click-to-sort headers with a sort caret. |
| initialSorting | SortingState | [] | The sort on mount, uncontrolled. |
| sorting | SortingState | - | The sort, controlled. Pair it with onSortingChange; initialSorting is then ignored. |
| onSortingChange | (sorting: SortingState) => void | - | Called after the sort changes; with sorting set, the change to apply. |
| enableRowSelection | boolean | false | Prepend a checkbox column with a select-all header that goes indeterminate on a partial selection. |
| onRowSelectionChange | (selection: RowSelectionState) => void | - | Called after each selection change, including rows dropping out because they left data. |
| renderSelectionActions | (rows: Row<TData>[], table: Table<TData>) => React.ReactNode | - | Bulk actions for the floating DataTableSelectionBar shown while rows are selected. Requires enableRowSelection. |
| onRowClick | (row: Row<TData>, event: React.MouseEvent | React.KeyboardEvent) => void | - | Open a row on a click anywhere on it or Enter on a focused row; clicks on controls inside the row do their own job. Makes body rows focusable. |
| activeRowId | string | null | - | The id of the row open elsewhere (a detail pane): painted with an ink tint and aria-current="true". |
| onDisplayedRowsChange | (rows: Row<TData>[]) => void | - | The rows shown, in display order, across all pages (searched, filtered, sorted). Called after mount and whenever that list changes, not on paging. |
| toolbar | boolean | false | Force the toolbar rail on. It turns on by itself with searchable, filters, toolbarActions, viewOptions or enableCardLayout. |
| searchable | boolean | false | Add a search box that filters every column (TanStack global filter). |
| searchPlaceholder | string | "Search for anything" | Placeholder of the search box. |
| globalFilter | string | - | The search text, controlled, with or without the built-in box. Pair it with onGlobalFilterChange. |
| onGlobalFilterChange | (value: string) => void | - | Called as the search text changes; with globalFilter set, the change to apply. |
| toolbarActions | React.ReactNode | - | The toolbar's right-hand slot for your own buttons. |
| filters | DataTableFilterField[] | - | Faceted filters: a multi-select per field in the toolbar plus removable chips below it. Filtered columns need filterFn: "arrIncludesSome". |
| onColumnFiltersChange | (filters: ColumnFiltersState) => void | - | Called after the column filters change. |
| viewOptions | boolean | false | Add the View options dropdown for showing and hiding labelled columns. |
| columnVisibility | VisibilityState | - | Visible columns, controlled ({ columnId: false } hides one). Pair it with onColumnVisibilityChange. |
| onColumnVisibilityChange | (visibility: VisibilityState) => void | - | Called after a column is shown or hidden. |
| enableCardLayout | boolean | false | Add a Rows/Cards switch to the view options (implies viewOptions) and render a card grid when chosen. |
| defaultLayout | "rows" | "cards" | "rows" | The layout on mount when enableCardLayout is on. |
| renderCard | (row: Row<TData>) => React.ReactNode | - | Replace the generated card (first column as title, labelled columns as fields, icon-only action top-right). |
| enableGrouping | boolean | false | Collapse rows into expandable groups by the initialGrouping column. |
| initialGrouping | GroupingState | [] | The column id(s) to group by. |
| renderSubRow | (row: Row<TData>) => React.ReactNode | - | A detail panel under an expanded row; prepends a caret toggle column. Not combined with enableGrouping. |
| getRowCanExpand | (row: Row<TData>) => boolean | - | Which rows can expand a renderSubRow panel; every row when omitted. |
| onRowReorder | (event: DataTableRowReorder<TData>) => void | - | Adds a drag grip per row; reports { row, fromIndex, toIndex } for you to apply with moveItem. Inert under a sort or grouping. |
| enableColumnOrdering | boolean | false | Drag headers (or arrow-nudge their grip) into a new order. Utility and meta.sticky columns stay put. |
| initialColumnOrder | string[] | - | Left-to-right column ids on mount; the columns order when omitted. |
| onColumnOrderChange | (order: string[]) => void | - | Called after the column order changes. |
| enablePagination | boolean | false | Page rows client-side with a Pagination toolbar below; the page steps back when data shrinks under it. |
| pageSize | number | 8 | Rows per page on mount. |
| pageSizeOptions | number[] | [8, 16, 24, 50, 100] | Rows-per-page choices; by default one, two and three pages of pageSize, then 50 and 100. The size in use is always merged in. |
| onPaginationChange | (pagination: PaginationState) => void | - | Called after the page or page size changes. |
| onLoadMore | () => void | - | Infinite scroll: called when a sentinel near the end scrolls into view. Replaces the pagination toolbar. |
| hasMore | boolean | false | More rows can still be loaded; the sentinel stops when false. |
| loadingMore | boolean | false | A load-more fetch is in flight: shows a loading row and blocks duplicate calls. |
| manualSorting | boolean | false | Sorting happens on the server; rows are not sorted client-side. |
| manualFiltering | boolean | false | Search and filters happen on the server; rows are not filtered client-side. |
| manualPagination | boolean | false | data is already the current page. Pass pageCount or rowCount. |
| pageCount | number | - | Total pages under manualPagination; derived from rowCount when omitted. |
| rowCount | number | - | Total rows across all pages, used to derive pageCount. |
| loading | boolean | false | Swap the rows for skeletons that mirror the columns; sets aria-busy and disables paging. |
| loadingRows | number | 5 | Skeleton rows (or cards) shown while loading. |
| emptyState | React.ReactNode | <DataTableEmpty /> | Shown with no rows. The default picks kind="search" while a search or filter is active, else kind="empty". |
| persistKey | string | - | Remember visible columns, column order, sort, layout and page size in localStorage under this key. Applied after mount. |
| Data attribute | Description |
|---|---|
| data-state | "selected" on a selected row (or card). |
| data-row-id | The row id on each body row while onRowReorder is set. |
| data-column-id | The column id on each header while enableColumnOrdering is on. |
ColumnMeta
Not a component: the keys Koala adds to TanStack's columnDef.meta, read by DataTable for each column's header and cells.
| Prop | Type | Default | Description |
|---|---|---|---|
| numeric | boolean | - | Right-align the header and cells with tabular-nums. |
| align | "left" | "center" | "right" | "left" | Text alignment for the header and cells. |
| sticky | "left" | "right" | - | Pin the column to an edge while the rest scrolls sideways. |
| label | string | - | The column's name in View options and as a card field label. Falls back to a string header; columns with neither cannot be hidden. |
| className | string | - | Extra classes for the column's header and cells. |
| headerTooltip | React.ReactNode | - | Appends a focusable info icon after the header label that shows this hint. |
| cellTooltip | (context: CellContext<TData, TValue>) => React.ReactNode | - | A tooltip per cell; return null or undefined to skip one. |
DataTableTabs
Line Tabs (Radix Tabs.Root) wrapped around a DataTable to split one list into views. The table inside becomes the tab panel and returns to its first page on a switch. Forwards all Tabs props except variant and orientation.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | The active view, controlled. |
| defaultValue | string | - | The view on mount, uncontrolled. |
| onValueChange | (value: string) => void | - | Called when the view changes. |
| density | "comfortable" | "compact" | "compact" | Tabs spacing. Falls back to the nearest DensityProvider. |
| Data attribute | Description |
|---|---|
| data-orientation | "horizontal", set by Radix. |
DataTableTabsList
The full-width tab rail, Radix Tabs.List; scrolls sideways on a phone. Give it an aria-label naming what the views split.
DataTableTab
One view, a TabsTrigger (Radix Tabs.Trigger); forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The view this tab selects. |
| count | number | - | A count chip after the label that rolls to each new figure. Omit for no chip. |
| static | boolean | true | Kept for compatibility; controls no longer scale on press. |
| disabled | boolean | false | Makes the tab unselectable. |
| Data attribute | Description |
|---|---|
| data-state | "active" or "inactive". |
| data-disabled | Present when the tab is disabled. |
useDataTableTabs
Hook returning the nearest DataTableTabs context, { value } with the active view, or null when the table stands on its own.
DataTableToolbar
The control rail for a hand-composed toolbar: two DataTableToolbarSections pushed to opposite edges, reflowing on a phone. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | - | Control height imposed on everything inside; a control's own size still wins. Unset, it follows the container's size, then density (compact gives sm). |
| Data attribute | Description |
|---|---|
| data-control-size | The resolved control size. |
DataTableToolbarSection
A cluster of controls inside the toolbar: left for search and filters, right for actions. Forwards all native <div> props.
DataTableSearch
A magnifier-prefixed search Input with a trailing shortcut hint. Forwards all InputField (native <input>) props and ref; className lands on the field.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | string | "Search for anything" | Placeholder text. |
| shortcut | React.ReactNode | false | "⌘K" | The Kbd hint on the right edge (display only; bind the key yourself). false hides it. |
| size | "sm" | "md" | "lg" | - | Control size; unset, it follows the toolbar around it. |
| rootClassName | string | - | Classes for the box (InputRoot), e.g. its width. Default width is full below sm, w-72 above. |
DataTableFacetedFilter
A dashed outline button opening a Popover of checkable options, each with its count of matching rows; writes an array to the column's filter value.
| Prop | Type | Default | Description |
|---|---|---|---|
| column | Column<TData, unknown> | - | The TanStack column to filter, from table.getColumn(id). It needs filterFn: "arrIncludesSome". |
| title* | string | - | The button label. |
| options* | DataTableFilterOption[] | - | The choices: { label, value, icon? }. |
DataTableActiveFilters
The row of removable chips for every active faceted value, plus a Reset button that clears all column filters.
| Prop | Type | Default | Description |
|---|---|---|---|
| table* | Table<TData> | - | The TanStack table instance. |
| filters* | DataTableFilterField[] | - | The same fields the filters were built from: { columnId, title, options }. |
DataTableViewOptions
The View options dropdown: an optional Rows/Cards switch above a checkbox per labelled, hideable column. The menu stays open while toggling.
| Prop | Type | Default | Description |
|---|---|---|---|
| table* | Table<TData> | - | The TanStack table instance. |
| enableLayout | boolean | false | Show the Rows/Cards switch. |
| layout | "rows" | "cards" | "rows" | The current layout, for the switch. |
| onLayoutChange | (layout: "rows" | "cards") => void | - | Called when the switch changes. |
| label | string | "View options" | Accessible name of the icon trigger. |
columnLabel
columnLabel(column) returns a column's display name: meta.label, else a string header, else undefined.
DataTableSelectionBar
The floating bulk-action pill (count, clear, actions), dark in every theme. An absolute overlay: put it inside a relative container. Forwards native <div> props to the overlay; className lands on the pill.
| Prop | Type | Default | Description |
|---|---|---|---|
| count* | number | - | How many rows are selected, shown as "N selected". |
| onClear* | () => void | - | Called by the clear (x) button. |
| children | React.ReactNode | - | The bulk-action controls, after a divider. |
DataTableEmpty
The zero-row placeholder, a compact EmptyState preset with an icon, title and description per kind.
| Prop | Type | Default | Description |
|---|---|---|---|
| kind | "empty" | "search" | "empty" | empty for no data yet; search for a search or filter that matched nothing. |
| icon | React.ReactNode | - | Replaces the preset icon. |
| title | React.ReactNode | - | Replaces the preset title. |
| description | React.ReactNode | - | Replaces the preset description. |
| children | React.ReactNode | - | The single action, e.g. an outline "Clear filters" Button. |
DataTableGrip
The drag handle DataTable renders for row and column reordering, exported for hand-built tables. Renders a <button> and forwards its props; arrow keys on the axis call onNudge.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | - | Accessible name, e.g. "Reorder row". |
| axis* | "rows" | "columns" | - | Rows move up and down, columns left and right. |
| onGrab | (event: React.PointerEvent<HTMLElement>) => void | - | Starts a drag on pointer down. |
| onNudge | (direction: -1 | 1) => void | - | The keyboard path: one step up/left or down/right. |
| locked | React.ReactNode | - | Why dragging is off: the grip goes inert (aria-disabled) and shows this as a tooltip. |
| dragging | boolean | - | Holds the handle visible and lit while its row or column is dragged. |
| Data attribute | Description |
|---|---|
| data-dragging | "true" while dragging. |
moveItem
moveItem(items, fromIndex, toIndex) returns a new array with one item moved; feed it an onRowReorder event.
Table
The styled <table> primitive, wrapped in its scroll container; forwards all native <table> props. Provides the variants to every primitive below.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "minimal" | "container" | "full-bleed" | "minimal" | Chromeless with flush edge cells, the ring-edged, rounded card, or edge to edge with the edge cells padded by --table-inset. |
| density | "comfortable" | "compact" | "compact" | Row height and cell padding. Falls back to the nearest DensityProvider. |
| striped | boolean | false | Zebra-stripe even body rows. |
| hoverable | boolean | true | Tint body rows on hover. |
| stickyHeader | boolean | false | Pin header cells to the top of the scroll container. |
| containerProps | React.ComponentProps<"div"> | - | Props for the scroll container, e.g. { className: "max-h-96" }. |
| CSS variable | Description |
|---|---|
| --table-surface | The opaque row surface every stripe, hover and pinned cell mixes into; set to var(--surface, var(--background)). |
| --surface | Read from the container (a Dialog, a Card) so the table blends into it. |
| --table-inset | full-bleed only: the padding of the first and last cells, so the text lines up with the pane's gutter. Defaults to 16px. |
TableHeader
Forwards all native <thead> props.
TableBody
Forwards all native <tbody> props; carries the hover, stripe and current-row tints.
TableFooter
A muted, tabular-nums totals band. Forwards all native <tfoot> props.
TableRow
Forwards all native <tr> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| selected | boolean | - | Paint the selected fill. |
| current | boolean | - | Mark the record open elsewhere: aria-current="true" and an ink tint, distinct from selected. |
| Data attribute | Description |
|---|---|
| data-state | "selected" when selected. |
TableHead
A header cell. Forwards all native <th> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "left" | "center" | "right" | "left" | Text alignment. |
| sticky | "left" | "right" | - | Pin the column to an edge while the rest scrolls sideways. |
TableCell
A body cell. Forwards all native <td> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "left" | "center" | "right" | "left" | Text alignment. |
| numeric | boolean | - | Right-align with tabular-nums. |
| sticky | "left" | "right" | - | Pin the cell to an edge; it inherits its row's background. |
TableCaption
Muted copy describing the table. Forwards all native <caption> props.
TableCellText
A two-line cell body: a primary label over muted supporting text. Compose it inside a TableCell.
| Prop | Type | Default | Description |
|---|---|---|---|
| primary* | React.ReactNode | - | The main line. |
| secondary | React.ReactNode | - | The muted line under it. |
TableEmpty
A full-width row with one cell spanning every column, for a TableBody with no rows.
| Prop | Type | Default | Description |
|---|---|---|---|
| colSpan* | number | - | How many columns the cell spans. |
| children | React.ReactNode | "No results." | The message. |