The backbone of every list screen. Sorting (Shift for multi-sort), locale-aware search, column visibility, order, widths and pinning, row selection with a bulk bar that can select all matching rows across pages, pagination, server mode with URL state, inline editing with rollback, virtual rows, a container-width card layout and CSV export — on one serializable ListState shared with Filter bar and Saved views.
| More actions | |||||||
|---|---|---|---|---|---|---|---|
Writing release notes people read | Scheduled | Tom Brennan | Product | 8,739 | |||
Hiring our first support engineer in Istanbul | Published | Tom Brennan | Company | 9,207 | |||
A checklist for safer bulk actions | In review | Tom Brennan | Engineering | 13,207 | |||
A field guide to customer interviews | Draft | Leila Haddad | Company | 17,877 | |||
Localising for Turkish: İ, ı and friends | Scheduled | Selin Kaya | Customers | 3,125 | |||
Designing empty states that convert | Published | Ada Park | Customers | 13,718 | |||
What we learned shipping dark mode | In review | Selin Kaya | Customers | 4,930 | |||
How we cut onboarding time in half | Published | Selin Kaya | Design | 16,113 | |||
Five dashboards every ops team needs | Published | Marcus Webb | Company | 15,076 | |||
Year in review: product | Published | Marcus Webb | Company | 14,934 |
1–10 of 18
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { DataTable, type DataTableColumn } from "@vamos-ui/react/data-table";
const columns: DataTableColumn<Contact>[] = [
{ id: "name", header: "Name", accessor: "name" },
{ id: "stage", header: "Stage", accessor: "stage", type: "enum", options: stageOptions },
{ id: "value", header: "Deal value", accessor: "value", type: "currency" },
];
<DataTable caption="Contacts" columns={columns} data={contacts} getRowId={(c) => c.id} selectable bulkActions={actions} />TanStack Table v9 is isolated inside the component; you write DataTableColumn objects, not TanStack ColumnDefs.
Glass budget: rows, sticky headers and pinned cells are opaque mixes — the only blurred layer is the card (and the floating bulk bar on mobile).
Dates in data are YYYY-MM-DD strings; money is a decimal string. Both are formatted with the locale provider (try tr-TR: ; CSV delimiter, ₺, İ/ı-aware search).
Examples
Server mode + URL state (CRM)
12,480 contacts. onStateChange is the single entry point — apply only the latest requestId. Select the page, then “Select all 12,480”.
No results
Filter bar + Saved views (HR)
Composed layout. Change a filter on a view to see Modified → Save / Save as new / Reset; the column menu’s “Filter by this column” opens the bar.
Jane Cooper | Support Lead | Support | Active | Ines Laurent | No | |
|---|---|---|---|---|---|---|
Kemal Rossi | Engineering Manager | Engineering | Active | Ines Laurent | No | |
Priya Mehta | Solutions Consultant | Sales | On leave | Kerem Yıldız | No | |
Lucas Iyer | Account Executive | Sales | Active | Ines Laurent | No | |
Amara Petrov | Solutions Consultant | Sales | Active | Kerem Yıldız | Yes | |
Elif Şahin | Brand Designer | Design | Active | Grace Whitfield | No | |
Noah Novak | Senior Engineer | Engineering | Active | Kerem Yıldız | No | |
Sofia Yıldız | Staff Engineer | Engineering | Active | Ines Laurent | No | |
Mateo Weber | Sales Development Rep | Sales | Active | Ines Laurent | No | |
Hana Moreau | Platform Engineer | Engineering | Active | Omar Haddad | No |
1–10 of 48
Inline editing with rollback
Text, select, currency, date and switch editors. Optimistic save with a spinner; a rejected save restores the value and shows why.
Jane Cooper | |||||
|---|---|---|---|---|---|
Kemal Rossi | |||||
Priya Mehta | |||||
Lucas Iyer | |||||
Amara Petrov | |||||
Elif Şahin | |||||
Noah Novak | |||||
Sofia Yıldız |
Click a value to edit. Enter saves, Escape cancels. The fourth row is locked and rolls back.
Column management
Resize (drag, ←/→, double-click to fit), pin to start/end, hide, reorder and density from View. storageKey remembers it per browser.
Tags | |||||||
|---|---|---|---|---|---|---|---|
Jane Costa | Ostrava Labs | jane@ostravalabs.example | Lead | $1,006.00 | Leila Haddad | Türkiye | — |
Clara Novak | Fernhill Media | clara@fernhillmedi.example | Qualified | $8,798.00 | Selin Kaya | Türkiye | ReferralTrial |
Ravi Brooks | Ostrava Labs | ravi@ostravalabs.example | Qualified | $96,544.00 | Selin Kaya | India | Referral |
Clara Okafor | Ostrava Labs | clara@ostravalabs.example | Qualified | $18,966.00 | Marcus Webb | Canada | RenewalEvent+2 |
Daniel Iyer | Pemberly Confectionery | daniel@pemberlyconf.example | Lead | $43,779.00 | Leila Haddad | United States | — |
Hugo Moreau | İstanbul Kahve Co. | hugo@istanbulkahv.example | Qualified | $91,394.00 | Tom Brennan | Türkiye | Trial |
Amara Whitfield | Ridgeway Logistics | amara@ridgewaylogi.example | Won | $64,896.00 | Marcus Webb | Germany | Inbound |
Sofia Silva | Pemberly Confectionery | sofia@pemberlyconf.example | Qualified | $43,100.00 | Tom Brennan | Netherlands | PartnerEvent+1 |
Nadia Cooper | Ridgeway Logistics | nadia@ridgewaylogi.example | Lead | $43,979.00 | Leila Haddad | Japan | Enterprise |
Hana Şahin | Anadolu Lojistik | hana@anadolulojis.example | Lead | $34,911.00 | Leila Haddad | Japan | InboundRenewal |
1–10 of 40
10,000 rows, virtualised
virtual + pagination="none": only the visible rows are rendered, inside a native table with aria-rowcount / aria-rowindex.
Selin Kaya | Updated | Invoice INV-2041 | 10.78.212.80 | |
|---|---|---|---|---|
Ada Park | Signed in | Contact · Jane Cooper | 10.26.21.194 | |
Kerem Yıldız | Exported | Contact · Jane Cooper | 10.139.231.104 | |
Leila Haddad | Invited | Invoice INV-2041 | 10.86.200.211 | |
Selin Kaya | Exported | API key ••••4f2a | 10.139.104.24 | |
Omar Haddad | Signed in | Role · Billing admin | 10.186.222.40 | |
Omar Haddad | Signed in | Report · Q3 pipeline | 10.204.114.102 | |
Marcus Webb | Deleted | Post · Release notes — November | 10.191.197.213 | |
Leila Haddad | Signed in | Report · Q3 pipeline | 10.136.168.175 | |
Selin Kaya | Signed in | Report · Q3 pipeline | 10.241.236.4 | |
Leila Haddad | Updated | Workspace settings | 10.189.78.74 | |
Selin Kaya | Invited | Role · Billing admin | 10.96.245.111 | |
Selin Kaya | Updated | Contact · Jane Cooper | 10.98.210.137 | |
Ada Park | Updated | Role · Billing admin | 10.27.165.112 | |
Marcus Webb | Exported | Invoice INV-2041 | 10.3.172.11 | |
Grace Whitfield | Invited | Deal · Northwind renewal | 10.112.122.7 | |
Grace Whitfield | Updated | Post · Release notes — November | 10.34.100.28 | |
Ada Park | Updated | Post · Release notes — November | 10.237.213.158 | |
Ada Park | Updated | Contact · Jane Cooper | 10.226.217.101 | |
Ada Park | Signed in | API key ••••4f2a | 10.5.232.223 |
Container-width cards
Under 640px of container width rows become cards: title, subtitle, badge, label:value meta, checkbox and actions.
| More actions | ||||||||
|---|---|---|---|---|---|---|---|---|
Jane Şahin | Kestrel Robotics | Lead | $19,385.00 | Leila Haddad | jane@kestrelrobot.example | |||
Zeynep Kimura | Quarry Analytics | Lost | $87,574.00 | Ada Park | zeynep@quarryanalyt.example | |||
Freya Okafor | Kestrel Robotics | Won | $17,818.00 | Selin Kaya | freya@kestrelrobot.example | |||
Tomás Dubois | Tessel Imports | Lead | $35,947.00 | Leila Haddad | tomas@tesselimport.example | |||
Hana Costa | Marrow Capital | Proposal | $5,568.00 | Ada Park | hana@marrowcapita.example |
Loading, empty, no results, error
First load shows skeleton rows shaped by column type; refreshing keeps the data under a thin bar; errors keep the last rows.
Custom toolbar with useDataTable
Compose the parts and drive the state yourself; table is the TanStack v9 instance.
Ethan Mehta | Ostrava Labs | Lost | $89,900.00 |
|---|---|---|---|
Clara Yıldız | Fernhill Media | Lead | $19,258.00 |
Zeynep Rossi | Anadolu Lojistik | Won | $46,598.00 |
Ethan Kimura | Halden Works | Proposal | $8,568.00 |
Sofia Andersen | Northwind Traders | Lead | $12,835.00 |
Ravi Dubois | Ridgeway Logistics | Lead | $41,764.00 |
Mateo Andersen | Pemberly Confectionery | Lead | $44,342.00 |
Yusuf Costa | Brightmoor Foods | Proposal | $85,568.00 |
1–8 of 60
API
DataTable
| Prop | Type | Default |
|---|---|---|
columnsColumn definitions (see below). | DataTableColumn<T>[] | — |
dataRows. Client mode: every row. Server mode: the current page (or every loaded row for load-more). | T[] | — |
getRowIdStable row id (selection, edits, keys). | (row: T) => string | — |
captionRequired accessible name. Hidden visually unless showCaption. | string | — |
showCaptionShow the caption above the table. | boolean | false |
modeServer mode turns off client sorting, search, filters and paging; you load rows from onStateChange. | "client" | "server" | "client" |
rowCountServer mode: total matching rows. | number | — |
state / defaultStateControlled (any subset) or initial q, filters, sort, page, columns, density, view. | Partial<ListState> | — |
onStateChangeEvery change with its reason (search, filters, sort, page, pageSize, columns, density, view, reset). Search is debounced 300 ms; filter, search and sort changes reset the page. | (next, reason, requestId) => void | — |
urlStateSync q, filters, sort, page, size and view (sv) with the URL. Options: prefix, keys, adapter, history, density. | boolean | UrlStateOptions | false |
storageKeyRemember column visibility, order, widths, pins and density in localStorage. | string | — |
loadingWithout data: skeleton rows. With data: refreshing bar, data stays. | boolean | false |
errorDanger alert above the table with a retry button; loaded rows stay. | { message, onRetry? } | null | — |
paginationPagination bar, a Load more button, an intersection-observed sentinel (with a button fallback) or nothing. | "pages" | "load-more" | "infinite" | "none" | "pages" |
pageSizesRows-per-page options; the first is the default size. | number[] | [25, 50, 100] |
onLoadMore / hasMoreFor load-more and infinite. | () => Promise<void> · boolean | — |
cursorsCursor pagination: set page to { cursor, size } in state and pass the neighbouring cursors. | { prev, next } | — |
virtualRender only visible rows (TanStack Virtual). Height defaults to 32rem. A dev warning fires past 300 rendered rows without it. | boolean | { estimateRowHeight?, overscan?, height? } | false |
selectableCheckbox column, Shift-click ranges, `x` toggles the focused row, Esc clears. | boolean | "single" | false |
selection / defaultSelection / onSelectionChange{ mode: "ids", ids } or, for “select all” in server mode, { mode: "all", except, filters, q, total } — resets when the query changes. | DataTableSelection | — |
rowSelectableA string disables the checkbox and explains why. | (row) => boolean | string | — |
bulkActionsUp to three inline, the rest in ⋯. confirm opens an Alert Dialog; run may return { undo }. | DataTableBulkAction<T>[] | — |
rowActions⋯ menu in a pinned last column (also in cards). | (row) => MenuEntry[] | — |
rowHref / onRowClickThe title cell becomes a real link; the whole row is clickable by pointer. | (row) => string · (row) => void | — |
onCellEditPersist an inline edit. Reject (an Error message is shown) to roll back. | ({ row, rowId, columnId, value }) => Promise<void> | — |
navigationgrid: WAI-ARIA data grid with arrow keys, Home/End, PageUp/PageDown, Ctrl+Home/End, Enter/F2 to edit. | "table" | "grid" | "table" |
readOnlyTurns off inline editing. | boolean | false |
empty / noResultsNo data at all vs. a search/filter that matches nothing (default has Clear filters). | ReactNode | — |
mobileUnder 640px of container width: cards, drop priority 3 then 2 columns, or scroll with a sticky first column. | "cards" | "priority" | "scroll" | "cards" |
exportAdds the Export menu: page, selected or all rows as CSV (UTF-8 BOM, locale delimiter, formula guard). | { filename, formatted?, onServerExport? } | — |
densityTable-local density; View › Density writes state.density. | "inherit" | "compact" | "comfortable" | "spacious" | "inherit" |
variantOwn surface, or full width for list screens. | "card" | "flush" | "card" |
toolbarExtra toolbar content in the default layout. | ReactNode | — |
shortcuts`/` search, Esc clear selection, `x` toggle focused row (within the table). | boolean | true |
meResolves “Me” in user filters. | { id, name } | — |
labelsOverride built-in strings (dataTable namespace). | Partial<DataTableLabels> | — |
childrenCompose DataTableToolbar, DataTableSearch, DataTableFilterBar, DataTableSavedViews, DataTableViewOptions, DataTableExport, DataTableContent, DataTableFooter yourself. | ReactNode | — |
DataTableColumn<T>
| Prop | Type | Default |
|---|---|---|
id / headerStable id and the label used in the header, column menu, CSV, cards and filter field. | string | — |
accessorValue of the cell. Omit for display-only columns. | keyof T | (row) => unknown | — |
typetext, number, currency, percent, date, datetime, enum, boolean, user, tags — formatting, alignment, sorting, operators and editor. | FieldType | "text" |
cellCustom cell ({ row, value, rowId, index, column }). | (ctx) => ReactNode | — |
optionsenum labels, badge tones and sort order (shared with the Filter bar). | FilterOption[] | — |
currencyISO 4217 for currency columns (default: the locale provider’s). | string | — |
sortable / sortValueSorting uses Intl.Collator(locale, { numeric: true }); enums sort by option order. | boolean · (row) => string | number | — |
searchableIncluded in the global search (default: text, enum, user, tags). | boolean | — |
filterFilter field overrides (quick, ops, icon, async options). | Partial<FilterFieldDef> | false | — |
edit{ type: text | number | currency | date | select | boolean, options?, validate? } or { type: custom, render }. | DataTableCellEditor<T> | false | — |
mobileRole in the card layout. | "title" | "subtitle" | "meta" | "badge" | "avatar" | "hidden" | — |
prioritymobile="priority": which columns drop first. | 1 | 2 | 3 | — |
exportValueCSV value (default: raw value). | (row) => string | number | null | — |
hideable / pinnable / resizableThe title column is never hideable. | boolean | true |
size / minSize / maxSizeWidths in px (defaults by type). | number | — |
defaultVisibleStart hidden (still available in View). | boolean | true |
useDataTable()
| Prop | Type | Default |
|---|---|---|
tableTanStack v9 instance (escape hatch). | ReactTable | — |
state / setState(patch, reason)The list state and its setter. | ListState | — |
selection / setSelectionCurrent selection. | DataTableSelection | — |
pageRows / filteredRows / totalRows on the page, after search + filters, and the total. | T[] · T[] · number | — |
filterBarPropsSpread onto a FilterBar placed anywhere inside. | FilterBarProps subset | — |
savedViewsBinding(views)activeId, current, onSelect, onReset for SavedViews. | SavedViewsProps subset | — |
exportCsv({ scope, filename, formatted })Download page, selected or all rows. | function | — |
List-state helpers
| Prop | Type | Default |
|---|---|---|
serializeListState / parseListStateReact-free; parse in a server component so the first HTML has the right rows. Unknown fields and operators are dropped. | (state, opts) ⇄ (search, opts) | — |
mergeListStateIntoSearchReplace the list’s own params and keep the rest. | (search, state, opts) => string | — |
createHistoryAdapter / createMemoryAdapterhistory.replaceState (works with Next.js useSearchParams) or in-memory. | UrlStateAdapter | — |
applyFilters / searchRowsThe client filter semantics a server should match (between inclusive, dates by calendar day in timeZone, relative resolved at query time). | function | — |
toCsv / escapeCsvCell / csvDelimiter / downloadCsvThe CSV writer behind Export. | function | — |
filterFieldsFromColumnsFilter bar fields from table columns. | (columns) => FilterFieldDef[] | — |