ui · Primitive
DataTable
@devalok/shilp-sutra/ui/data-tableView in Storybook Hand-curated previews ship in rolling waves. See it live in Storybook →
Reference
Props
columns: ColumnDef<TData>[] (TanStack column definitions)
data: TData[]
sortable: boolean — enable column sorting
onSort: (key: string, dir: 'asc' | 'desc' | false) => void — server-side sort callback (enables manualSorting)
filterable: boolean — enable per-column filters
globalFilter: boolean — enable global search
paginated: boolean — enable client-side pagination
pagination: { page: number, pageSize: number, total: number, onPageChange: (page: number) => void } — server-side pagination (1-based page)
pageSize: number (default 10)
selectable: boolean — enable row selection with checkboxes
selectedIds: Set<string> — controlled selection state
selectableFilter: (row: TData) => boolean — disable selection on certain rows
getRowId: (row: TData) => string — custom row ID accessor
onSelectionChange: (selectedRows: TData[]) => void
expandable: boolean — enable row expansion
renderExpanded: (row: TData) => ReactNode — expanded row content
singleExpand: boolean — only one row expanded at a time
loading: boolean — show skeleton shimmer rows
emptyState: ReactNode — custom empty state (takes precedence over noResultsText)
noResultsText: string (default "No results.")
stickyHeader: boolean — sticky table header
onRowClick: (row: TData) => void — row click handler (excludes interactive element clicks)
bulkActions: BulkAction<TData>[] — floating action bar on selection — { label, onClick, color?: 'default'|'error', disabled? }
toolbar: boolean — show DataTableToolbar (column visibility, density, CSV export)
editable: boolean — enable double-click cell editing
virtualRows: boolean — virtualize rows for large datasets
columnPinning: { left?: string[], right?: string[] }
defaultDensity: 'compact' | 'standard' | 'comfortable'
Defaults
pageSize=10, noResultsText="No results."
Example
import { DataTable } from '@devalok/shilp-sutra/ui/data-table'
<DataTable
columns={[
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
]}
data={users}
sortable
onSort={(key, dir) => handleSort(key, dir)}
pagination={{ page, pageSize: 20, total: totalCount, onPageChange: setPage }}
loading={isLoading}
emptyState={<EmptyState title="No users" />}
/>
Composability
Server vs client mode is prop-driven, not explicit.
- Pass
onSort→ server-side sort (manual, rows stay in data order — you're responsible for re-fetching). - Pass
paginationobject → server-side pagination (manual, pass total count). - Omit both → client-side sort/pagination via TanStack react-table.
- Mix-and-match:
onSort+ no pagination = server sort + client pagination.
Companion components:
DataTableToolbar— enabled viatoolbar={true}. Provides column visibility, density switcher, CSV export. Reads table state viaDataTableContext(internal). Rendered ABOVE the table automatically.BulkActionBar(floating) — appears when rows are selected ANDbulkActionsarray is non-empty. Synced withselectedIds; shows count + action buttons.EmptyStatefrom@devalok/shilp-sutra/composed— pass toemptyStateprop. Takes precedence overnoResultsTextstring.
Controlled selection:
- Pass
selectedIds(Set<string>) +onSelectionChangefor controlled row selection. - Provide
getRowId: (row) => row.idso selection survives data refetches (otherwise TanStack uses array index, which breaks on sort/filter). selectableFilter: (row) => booleandisables selection on specific rows (e.g. archived items).
Row click model:
onRowClickfires on row-level click BUT excludes clicks on checkboxes, buttons, links, and inputs automatically. No manualstopPropagationneeded for standard interactive elements.
Virtualization: virtualRows={true} enables row virtualization via @tanstack/react-virtual. Turn it on for 1000+ row datasets; the scroll container must have a bounded height.
Density integration: defaultDensity="compact" is the Karm-style dense mode (h-9 rows). DataTableToolbar's density switcher updates this at runtime; the prop sets the initial state only.
Gotchas
- Barrel-isolated since v0.5.0 — must use
@devalok/shilp-sutra/ui/data-table, NOT theuibarrel - Requires @tanstack/react-table and @tanstack/react-virtual as peer dependencies
- When onSort is provided, sorting is manual (server-side) — rows stay in data order
- When pagination prop is provided, pagination is manual — pass total count
- selectedIds syncs via useEffect — provide getRowId for custom row IDs
- onRowClick does NOT fire when clicking checkboxes, buttons, links, or inputs
- Use defaultDensity="compact" for Karm-style h-9 rows
virtualRows={true}requires a bounded scroll container — unbounded height silently disables virtualization
Changes
v0.29.0
- Fixed Controlled selection infinite re-render loop — inline
getRowIdcallback causedonSelectionChangeeffect to fire every render, creating a setState cycle withselectedIds. Now uses a stable ref forgetRowId.
v0.16.1
- Fixed
serverPaginationobject reference inuseCallbackdependency caused stale closure — now uses stable ref foronPageChange - Fixed
onSelectionChangeeffect fired every render due totablein dependency array — now derives selected rows directly - Fixed
selectedRowsuseMemo for bulk actions had sametabledependency issue
v0.16.0
- Added
onSortcallback for server-side sorting - Added
emptyStateReactNode slot - Added
loadingprop with shimmer skeleton rows - Added
selectedIds+selectableFilterfor controlled selection - Added
paginationprop for server-side pagination - Added
singleExpandprop - Added
stickyHeaderprop - Added
onRowClickhandler - Added
bulkActionsfloating action bar
v0.5.0
- Changed (BREAKING) Removed from
@devalok/shilp-sutra/uibarrel export — must use@devalok/shilp-sutra/ui/data-table
v0.1.1
- Fixed
useEffectexhaustive-deps with proper dependency array
v0.1.0
- Added Initial release