build69
build69 Β /Β  Components Β /Β  Data Display Β /Β  DataTable

DataTable

v1.0.29Updated 4 Sept 2026

Professional sortable, filterable, groupable table powered by TanStack Table. Use for any tabular list where users need to slice the data.

Preview

Live Β· 51 stories
β–ΈWhy this is recommendedtablelistgridsortable+27

The default, out-of-the-box DataTable.

Recommended for+14
  • βœ“Listing invoices, customers, products, employees, transactions
  • βœ“Any dataset > 25 rows where users need sort / filter / group
  • βœ“Admin dashboards requiring bulk operations
  • βœ“Reporting tables that benefit from grouping by status / category
  • βœ“Spreadsheet-like surfaces needing cell range copy or Excel export
  • βœ“Tables where clicking a row expands an inline master-detail panel of related records
Tags+6
tablelistgridsortablefilterablepaginatedselectionexportimportcsvtanstackexcelmaster-detailreordercell-selectionstripedzebragrid-linesspreadsheeteditablefill-handlepastei18nlocalelabelsthaifield-typesemailtelcurrencydefault
Features+4
  • Column field types (DataTableColumn.type): email/tel/url render as links (url scheme-guarded to http(s)); number/currency/date/datetime format through the app locale (useB69Locale β€” Thai gets Buddhist years); boolean renders an icon. Right-aligns number/currency, centers boolean. An explicit `cell` always wins; filtering/sorting stay on the raw value.
  • Multi-column sorting (shift-click)
  • Per-column filtering (text, number, date, enum, boolean)
  • Global search (debounced, Cmd+K)
  • Per-column filters (text / number / date / enum / boolean) via header popovers, OR a left filter panel (enableFilterPanel) with a Filters toolbar button β€” one control per filterable column, faceted enum counts + number min/max, active-filter badge. The panel scrolls independently of the table (its own overflow, so a long filter list never grows the table row) and is drag- or keyboard-resizable between 200 and 560px. In page-flow mode (maxHeight="none") it sticks to the viewport instead, so it stays on screen and scrolls on its own while the rows scroll past β€” set --b69-filter-panel-sticky-top to your app bar height to clear it; it publishes its live width as --b69-filter-panel-width, and reports data-state="open"/"closed" on the data-table-filter-panel slot
  • Group by with per-group aggregates (sum, mean, min, max, count, unique)
  • Row selection with bulk actions bar
  • Icon-based row actions (getRowActions): inline quick-action icon buttons with tooltips + "β‹―" overflow menu; danger tone, per-row disabled
  • Column visibility / pinning / resizing
  • Column drag-and-drop reorder (enableColumnReorder)
  • Cell range selection with copy-to-clipboard TSV (enableCellSelection)
  • Row pinning sticky to top or bottom (enableRowPinning)
  • Excel export with per-column formatting (enableExcelExport, meta.excelFormat)
  • Pivot mode (pivot prop): spreadsheet-style matrix with rows / columns / aggregated values
  • Pivot multi-level column headers + row / column / grand totals
  • Excel export of the pivot matrix
  • Inline cell edit
  • Density toggle (compact / normal / comfortable)
  • Sticky header, sticky pinned columns
  • Zebra striping (striped prop) β€” frozen columns stripe too
  • Grid line / separator control (gridLines: none | horizontal | vertical | both)
  • Excel-style spreadsheet mode (spreadsheetMode): row-number gutter, active-cell keyboard nav, type-to-edit / F2, fill handle, paste-to-fill (Ctrl/Cmd+V), batched onCellsChange
  • CSV export of filtered + sorted rows
  • CSV import via toolbar (enableImport + onImport)
  • MasterDetail expandable row content (masterDetail render prop)
  • View presets (save / restore filter + sort + group + visibility)
  • Keyboard navigation
  • Loading / empty / error states
  • Sub-rows / expandable hierarchy
  • i18n: labels prop overrides every built-in UI string (toolbar, pagination, selection/bulk bar, empty/error states, aria-labels); counts format through LocaleProvider (Thai numerals, Buddhist calendar apps)
Not recommended forβˆ’2
  • βœ•Small lists (< 10 rows β€” use Table or KVList)
  • βœ•A simple, plain, static table with no interactivity (use Table)
  • βœ•Hierarchical tree data primary (use FileBrowser)
  • βœ•Card-based gallery layouts (use a grid of Cards)

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "DataTable" })

Usage

import { DataTable } from '@/components/data-display/data-table';

<DataTable
  data={invoices}
  columns={[
    { id: 'number', header: 'Invoice', accessorKey: 'number' },
    { id: 'customer', header: 'Customer', accessorKey: 'customer' },
    { id: 'amount', header: 'Amount', accessorKey: 'amount', meta: { align: 'right', mono: true } },
  ]}
  rowSelection="multi"
  bulkActions={[{ id: 'send', label: 'Send', onClick: rows => sendAll(rows) }]}
/>

Tick variants in the left menu β€” each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
data

Array of row objects

T[]yes
columns

Column definitions

DataTableColumn<T>[]yes
rowSelection

Enable row selection

boolean | 'multi' | 'single'β€”
bulkActions

Selection toolbar actions

BulkAction<T>[]β€”
getRowActions

Per-row actions column. Mark an action `inline: true` (with an icon) to render it as an icon button with its label as tooltip (visible by default β€” see `rowActionsReveal`); the rest collect in a "β‹―" overflow menu. `tone: 'danger'` for destructive styling, `disabled` grays out without hiding. Keep 2–3 inline, long tail in the menu.

(row: T) => DataTableRowAction[]β€”
rowActionsReveal

When the row-action icons show. 'always' (default) keeps them on screen β€” most discoverable, and the only option that works on touch. 'hover' reveals them on row hover / keyboard focus / open menu (denser Linear/Attio pattern, but invisible on touch).

'always' | 'hover'β€”
views

Saved view presets

DataTableView[]β€”
state

Top-level UI state

'loading' | 'empty' | 'error'β€”
onRowClick

Row click handler

functionβ€”
enableFilterPanel

Show a Filters button in the toolbar that toggles a left-hand filter panel, wired to the same column filters as the header popovers. Off by default. PREREQUISITE: the panel lists one control per column that declares `meta.filterType` ('text'|'number'|'date'|'enum'|'boolean') AND has a resolvable value (an accessorKey/accessorFn or matching id) β€” columns without a filterType are skipped, so enabling the prop on a table whose columns declare none renders an empty panel. Give each filterable column a meta.filterType.

booleanβ€”
filterPanelMode

Placement of the filter panel (needs enableFilterPanel). 'rail' (default) is an inline sibling that takes 256px when open β€” best for a full-width table. 'overlay' floats the panel over the table's left edge with a dismiss backdrop so the table keeps its full width β€” use inside Sheets/drawers/narrow panes where a rail would crush the columns. 'auto' is rail on wide containers, overlay once the container narrows (<560px). Default 'rail' is byte-identical. MCP feedback fb_903212e0.

'rail' | 'overlay' | 'auto'β€”
filterPanelResizable

Let the user drag the filter panel's right edge to resize it. On by default β€” the handle is a transparent 6px strip that only paints on hover/focus, and the panel still opens at defaultFilterPanelWidth, so nothing moves until it is dragged. Keyboard-resizable too: the handle is a focusable ARIA separator (arrows nudge 8px, Shift+arrow 32px, Home/End snap to the bounds). Set false for a fixed-width panel.

booleanβ€”
defaultFilterPanelWidth

Width (px) the filter panel opens at. Default 256 β€” the previous fixed width, so an untouched panel is unchanged.

numberβ€”
filterPanelWidth

Controlled filter-panel width (px). Pass with onFilterPanelWidthChange to own the width β€” e.g. to persist it per user. Clamped to filterPanelMinWidth/filterPanelMaxWidth on read, so a stored value can never render the panel out of bounds.

numberβ€”
onFilterPanelWidthChange

Fires on every resize step with the new clamped width (px). Use with filterPanelWidth for a controlled panel, or alone to persist an uncontrolled one.

functionβ€”
filterPanelMinWidth

Lower resize bound (px). Default 200 β€” below this the enum option rows wrap badly.

numberβ€”
filterPanelMaxWidth

Upper resize bound (px). Default 560 β€” above this the panel crowds out the table it filters.

numberβ€”
columns[].meta.headerHelp

Renders a "?" beside the column header explaining what the column means. A node gives a one-liner; { title, body } gives a heading plus detail. Composing this yourself needs a function `header`, which loses the plain-string header ergonomics (filter chips, the column picker and export all read `label`) and repeats on every column. The trigger is deliberately a SIBLING of the sort control, never a child: a button inside the sort button is invalid HTML and a React 19 SSR hydration error. On a column that opts in, the sort hit area shrinks from the whole header cell to the label. MCP feedback fb_54569a6f.

React.ReactNode | { title, body }β€”
persistFilterPanelWidthKey

localStorage key that persists the UNCONTROLLED panel width across sessions. Ignored when filterPanelWidth is controlled. The stored value is adopted after mount (never during render), so it cannot cause a hydration mismatch. Best-effort β€” a storage failure in private mode is swallowed. MCP feedback fb_c860aaf0.

stringβ€”
defaultFilterPanelOpen

Whether the filter panel starts open. Default false (unchanged).

booleanβ€”
filterPanelOpen

Controlled filter-panel open state. Pass with onFilterPanelOpenChange to own it β€” e.g. to restore whether the user left the panel open on their last visit. MCP feedback fb_4c48a1f5.

booleanβ€”
onFilterPanelOpenChange

Fires whenever the panel opens or closes β€” the toolbar button, the panel close button, or the overlay dismiss backdrop. Use with filterPanelOpen for a controlled panel, or alone to persist an uncontrolled one.

functionβ€”
enableColumnReorder

Drag-and-drop column reorder

booleanβ€”
enableCellSelection

Cell range selection + copy TSV

booleanβ€”
enableRowPinning

Pin rows to top / bottom

booleanβ€”
enableExcelExport

Excel export action (uses meta.excelFormat)

booleanβ€”
striped

Alternate row background (zebra striping); frozen columns stripe too

booleanβ€”
gridLines

Separator display (default 'horizontal' = row dividers only)

'none' | 'horizontal' | 'vertical' | 'both'β€”
layout

Column width strategy. 'fit' fills the container β€” flexible columns (no explicit size, unpinned, not user-resized) share the remaining width so the table never overflows or clips a trailing column; explicit-size / system / pinned columns keep their widths. 'auto' (default) lets the table scroll when columns exceed the viewport.

'auto' | 'fit'β€”
enableImport

Show a toolbar CSV Import button

booleanβ€”
onImport

Receives parsed CSV records (keyed by header) on import

(rows: Record<string, string>[], file: File) => voidβ€”
spreadsheetMode

Excel-style mode: row-number gutter, active-cell keyboard nav, type-to-edit/F2, fill handle, paste-to-fill (implies cell selection; editing needs editable columns)

booleanβ€”
onCellsChange

Batched change callback for spreadsheet fill/paste/edits (falls back to onCellEdit per cell)

(changes: { rowId: string; columnId: string; value: unknown }[]) => voidβ€”
masterDetail

Render expandable detail content per row

(row: T) => ReactNodeβ€”
filteredEmptyState

Shown when active filters/search reduce a NON-empty dataset to zero rows (defaults to a "no matching rows" message). Distinct from emptyState β€” which is only shown when the dataset is genuinely empty β€” so a "create your first record" prompt never renders on a filtered-to-zero table.

React.ReactNodeβ€”
pivot

Render a spreadsheet-style pivot matrix from the data (multi-level column headers + row / column / grand totals + Excel export)

{ rows: string[]; columns: string[]; values: { field: string; aggregation: string; label?: string; formatter?: (v: number) => string }[]; showRowTotals?: boolean; showColumnTotals?: boolean; grandTotal?: boolean }β€”
labels

Override any built-in UI string β€” toolbar (search placeholder, density, Columns/Views/Group, Export), pagination ('Showing {range} of {total}', '{count} / page', '{page} / {pageCount}'), selection ('{count} selected', select-all/select-row aria), empty/error states, filter-panel controls (operator options, value/min/max placeholders, option search, boolean Either/Yes/No, date-range prompt), row-actions and sort/pin/resize aria-labels. Strings with {token} placeholders are templates. Defaults: DEFAULT_DATA_TABLE_LABELS (English, exported). Wrap the app in LocaleProvider to also format all counts per locale (e.g. Thai numerals).

Partial<DataTableLabels>β€”

Related