@bloomskill/table-engine
v0.47.0
Published
Headless React data-grid engine on TanStack Table v9 — sorting, filtering, pagination, grouping, inline editing, cell & row selection, clipboard, CSV/Excel export and virtualization. Pair with the MUI or shadcn skin, or stay fully headless. TypeScript, MI
Downloads
201
Readme
@bloomskill/table-engine
The headless engine for Bst-Table — a small, UI-agnostic React data grid built on
TanStack Table v9. It ships the correct state engine
(sorting, filtering, editing, selection, …) plus a neutral, theme-able renderer with zero
component-library styling, so you pair it with a skin:
@bloomskill/table-mui,
@bloomskill/table-shadcn, or your own.
- 🪶 Two lightweight deps —
@tanstack/react-table+@tanstack/react-virtual;react/react-domare peers. - 🎛 Every feature is a toggle — data features on by default, heavy features opt-in.
- 🛰️ One grid, every scale — the same component runs client-side or against a server
DataSource(1M rows).
📚 Documentation
Full guides, a complete API reference and live, editable demos are on the docs site — gitofkumarsathish.github.io/bst-grid:
| Guide | Reference | More | | :-- | :-- | :-- | | Getting Started | Feature Guides | Styling & Theming | | Installation | Cell Types | AI Agents & MCP | | Recipes | API Reference | Coverage & Roadmap |
The docs are generated from the library's source and refreshed on every release, so they track the code and stay accurate as the API grows — an actively maintained reference, not a one-off snapshot.
Contents
Start here · Install · Quick start · Live examples · Feature map
Columns — how to shape and customize each column
· Column reference
· meta reference
· Cell types
· cellMeta by cell type
· Files columns — images & PDFs
Grid options
· useBstTable options
Feature guides — use it + customize it · Editing & validation · Batch editing · Selection, keyboard & clipboard · Access control · Row selection · Undo / redo · Filtering · Grouping · Column layout · Row layout · Conditional formatting · Cell spanning · Custom CSS · Body icons · Runtime settings sheet · Grid state (save / restore views) · Server mode (DataSource)
Reference · Exports · Requirements · License
Install
npm install @bloomskill/table-engine react react-dom@tanstack/react-table and @tanstack/react-virtual ship as dependencies — you don't install them separately.
Quick start
▶ Try it live — editable in your browser, no install.
import { useBstTable, BstTable, type BstTableColumn } from '@bloomskill/table-engine'
import '@bloomskill/table-engine/styles.css'
type Person = { id: string; name: string; age: number }
const data: Person[] = [
{ id: '1', name: 'Ada Lovelace', age: 36 },
{ id: '2', name: 'Linus Torvalds', age: 54 },
]
const columns: BstTableColumn<Person>[] = [
{ id: 'name', accessorKey: 'name', header: 'Name', sortFn: 'alphanumeric' },
{ id: 'age', accessorKey: 'age', header: 'Age', sortFn: 'basic' },
]
export function People() {
const table = useBstTable({ data, columns, getRowId: (r) => r.id })
return (
<div className="bst-table-root">
<BstTable table={table} />
</div>
)
}What renders where.
BstTablerenders the grid body — sortable, resizable headers + rows. Toolbars, search boxes and pagination bars are adapter chrome (MUI / shadcn), or you build your own against the returnedtableinstance..bst-table-rootapplies the theme variables.
Examples
Every feature and cell type has a live, editable demo on the documentation site — tweak the code and watch the grid update, right in the browser, no install.
Prefer a full, cloneable project? Seven standalone starters live in examples/
(quick-start, editing, cell-types, conditional-formatting, cell-spanning, server-mode,
field-formats). Run one locally:
cd examples/quick-start && npm install && npm run devFeature map
Every capability is a per-instance toggle, in one of two layers:
| Prefix | Layer | Resolved in | Example |
| --- | --- | --- | --- |
| enable* | engine behaviour (does the capability run) | useBstTable | enableSorting, enableEditing |
| show* | adapter chrome (does the control render) | the adapter | showSearch, showPagination |
Data features default on (opt-out); heavy/opinionated features (editing, selection, clipboard)
default off (opt-in). Pass boolean to toggle, or an options object to enable with
settings (e.g. pagination={{ pageSize: 25 }}). Follow a link for the full guide.
🔢 Data operations
| Feature | Turn on with | Default |
| --- | --- | --- |
| Sorting | enableSorting | true |
| Global search | enableGlobalFilter | true |
| Find (highlight + jump) | enableFind | false |
| Column filters | enableColumnFilters | true |
| Filter builder UI | <BstFilterBuilder> / enableColumnFilterRow | false |
| Pagination | pagination | true |
| Grouping + aggregates | enableGrouping | false |
🧱 Columns & layout
| Feature | Turn on with | Default |
| --- | --- | --- |
| Show / hide columns | enableHiding | true |
| Column resizing | enableColumnResizing | true |
| Auto-size to content | double-click the resize handle | — |
| Column pinning | enableColumnPinning | false |
| Column reordering | enableColumnOrdering | false |
| Fit to viewport (no h-scroll) | fitColumns | false |
| Responsive hiding | enableResponsive + meta.responsivePriority | false |
| Sticky-header viewport | enableStickyHeader | false |
| Per-column filter row | enableColumnFilterRow | false |
📋 Rows
| Feature | Turn on with | Default |
| --- | --- | --- |
| Row selection (checkboxes) | enableRowSelection | false |
| Add / delete / duplicate | enableRowActions | false |
| Master-detail panel | enableExpanding + renderDetail | false |
| Row pinning (freeze top/bottom) | enableRowPinning | false |
| Row resizing (drag height) | enableRowResize | false |
| Virtualization (row/column) | enableVirtualization (+ enableColumnVirtualization) | false |
✏️ Editing
| Feature | Turn on with | Default |
| --- | --- | --- |
| Inline editing | enableEditing | false |
| Batch editing + one onSave | enableEditing: { mode: 'batch' } | false |
| Validation (sync / async / cross-column) | enableValidation | false |
| Undo / redo | enableUndoRedo | false |
| Type-to-edit (spreadsheet entry) | enableTypeToEdit | false |
⌨️ Selection & clipboard
| Feature | Turn on with | Default |
| --- | --- | --- |
| Cell / range selection + keyboard nav | enableCellSelection | false |
| Copy / paste (TSV) | enableClipboard | false |
| Copy whole column / row | enableCopyColumn / enableCopyRow | true |
| Access control (disable cascade) | disabled · rowDisabled · meta.disabled · cellDisabled | — |
🎨 Cells, styling & scale
| Feature | Turn on with | Default |
| --- | --- | --- |
| Cell-type registry (17 types) | meta.type | 'text' |
| In-cell charts (sparkline / KPI) | meta.type: 'sparkline' \| 'kpi' | — |
| QR · barcode · rich text | meta.type: 'qr' \| 'barcode' \| 'richText' | — |
| ERP field formats (Aadhaar · PAN · GSTIN · IBAN · Luhn …) | cellMeta.pattern on text / number | — |
| Width-aware chips (fit to column) | cellMeta.fitChips on multiSelect | false |
| File preview + upload/delete (B5/I3) | meta.type: 'files' + cellMeta.onUpload/onDelete · PDF thumbnail cellMeta.pdfThumbnail | click-to-preview on |
| Cell spanning (merge cells) | enableCellSpanning | false |
| Custom CSS slots | classNames / styles | — |
| Conditional formatting | conditionalFormats + enableConditionalFormatting | on when rules present |
| Injectable body icons | icons | built-in SVGs |
| Runtime settings sheet | useBstSettings (showSettings in adapters) | false |
| Grid state save/restore (X21) | useBstGridState / loadGridState (gridState={{ key }} in adapters) | off (opt-in) |
| Server-side DataSource | useBstDataSource(source) | client mode |
Column reference
A Bst-Table column is a TanStack v9 ColumnDef with a typed meta slot. Everything
column-specific — the cell type, whether it's editable, formatting, access control, per-column
CSS — lives in meta.
import type { BstTableColumn } from '@bloomskill/table-engine'
const columns: BstTableColumn<Person>[] = [
{
id: 'salary', // stable column id (used by selection, copy, filters…)
accessorKey: 'salary', // key into the row object (or use accessorFn)
header: 'Salary', // header content (string or renderer)
sortFn: 'basic', // built-in: 'basic' | 'alphanumeric' | 'datetime'
minSize: 90, // TanStack sizing (px) — also maxSize / size
meta: {
type: 'number', // ← selects the cell renderer/editor
editable: true, // ← opt into inline editing
align: 'right',
cellMeta: { currency: 'USD', precision: 2 }, // ← per-type settings
},
},
]
header,accessorKey/accessorFn,size/minSize/maxSize,sortFn,filterFn,aggregationFn,enableSorting,enableHiding, … are standard TanStack v9 column fields — the TanStack column docs apply. The Bst-Table additions all live undermeta.
meta reference
columnDef.meta is typed as BstColumnMeta. Every field is optional.
| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| type | string | 'text' | Which cell type renders/edits this column. |
| editable | boolean \| (row) => boolean | false | Opt the column into inline editing — statically, or per row. |
| disabled | boolean \| (row) => boolean | — | Access control (F3/F4): disable the whole column, or per row/cell. A disabled cell is muted and non-editable, but stays selectable/copyable. |
| options | BstOption[] | — | Choices for singleSelect / multiSelect / radio. |
| cellMeta | object | — | Per-type settings (precision, variant, maxChips, required, …). |
| format | string \| Intl options | — | Number/date display: shorthand ('currency', 'percent') or an Intl.*FormatOptions bag. |
| locale | string | host locale | BCP-47 locale for number / date formatting + parsing. |
| placeholder | string | — | Placeholder shown in an empty editor. |
| editMode | 'inline' \| 'popup' | cell-type default | Force in-cell vs. dialog editing for this column. |
| validate | (value, ctx) => FieldError[] \| Promise<…> | — | Column validator; runs after the cell-type validator. ctx.getSiblingValue(id) enables cross-column rules; return a Promise for async. |
| align | 'left' \| 'center' \| 'right' | 'left' | Text alignment for read + edit. |
| cellClassName | (props) => string \| undefined | — | Conditional class on the body cell (K1/K3). |
| cellStyle | (props) => CSSProperties \| undefined | — | Conditional inline style / CSS vars on the body cell. |
| headerClassName | string | — | Extra class on this column's header <th>. |
| headerStyle | CSSProperties | — | Inline style / CSS vars on this column's header <th>. |
| rowSpan | 'group' | — | Cell spanning (A5): auto-merge vertically-consecutive equal values. Needs enableCellSpanning. |
| responsivePriority | number | 0 | Responsive hiding (G4): higher stays visible longer. Needs enableResponsive. |
| actions | { edit?, delete?, duplicate?, view? } | { edit, delete } | Which buttons the action / actionMenu cell renders (B10). |
Cell types
▶ See every cell type live — editable demos, no install.
Pick the renderer + editor with meta.type. Read renderers are dependency-free and run on the
hot path; the MUI / shadcn adapters supply richer editors for the same types via their presets.
| meta.type | Renders | Value shape | Editable | Notable cellMeta |
| --- | --- | --- | --- | --- |
| text (default) | single-line text, ellipsis + title tooltip | string | ✅ input | required, pattern |
| longText | clamped multi-line text | string | ✅ textarea → popup (adapters) | required |
| number | locale number / currency / percent | number \| null | ✅ number input | precision, currency, useGrouping, required, pattern |
| dateTime | date / time / datetime | string \| Date \| null | ✅ native picker | variant, required |
| boolean | check ✓ / muted dash | boolean | ✅ checkbox | — |
| singleSelect | badge (color · icon · avatar) | string \| null | ✅ dropdown | via meta.options |
| multiSelect | chips + +N more overflow | string[] | ✅ checkbox dropdown | maxChips, fitChips |
| radio | badge | string \| null | ✅ radio group | layout |
| hyperlink | anchor | string or { href, label } | ✅ url input | target |
| files | image thumbnail / PDF thumbnail (pdfThumbnail) / icon + name · click to preview (image inline, PDF native viewer) | FileRef[] ({ name, url, thumbnailUrl?, contentType? }) | popup: add/remove (adapters) | preview, pdfThumbnail, onUpload, onDelete, accept |
| sparkline | inline SVG line / area / bar | number[] (or "1,2,3") | read-only | variant, width, height, color, min, max, showValue |
| kpi | value + trend delta chip + mini-spark | number or { value, delta?, data? } | read-only | invertDelta, deltaPercent, sparkWidth |
| qr | inline-SVG QR code (byte mode, v1–10) | string | ✅ input | ecLevel, size, margin |
| barcode | inline-SVG Code 128 | string | ✅ input | height, showText |
| richText | sanitized-HTML preview or formatted | HTML string | ✅ toolbar → popup (adapters) | render |
| action | inline Edit / Save / Copy / Delete buttons | — | — | via meta.actions |
| actionMenu | compact ⋯ kebab → actions popup | — | — | via meta.actions |
Bring your own type. Register custom renderers/editors with
createCellTypeRegistry+defineCellTypeand pass them ascellTypes(adapters start fromcreateMuiPreset()/createShadcnPreset()— extend those). See Exports.
cellMeta by cell type
meta.cellMeta is free-form per-type settings. A field common to every editable type is
required (a non-empty check the validator enforces). The rest:
text / number — field formats (ERP, Frappe-style). cellMeta.pattern applies a named
validation + input-mask + normalizer preset — the identity / finance fields an ERP form needs,
validated and masked without a hand-written validate per column:
| cellMeta | Type | Effect |
| --- | --- | --- |
| pattern | preset name · RegExp · FieldFormat | Validate + mask + normalize the cell as that format. |
| patternMessage | string | Error message to show when pattern is a bare RegExp. |
Built-in names: aadhaar (12-digit, Verhoeff checksum, masked #### #### ####) · pan ·
gstin (15-char, mod-36 checksum) · tan · ifsc · email · phone (India mobile) ·
pincode · url · upi · passport · iec · esic (17-digit) · pf (12-digit UAN) ·
iban (mod-97 checksum, grouped) · swift (BIC) · creditCard (Luhn, grouped). Register
your own with defineFieldFormat or by adding to FIELD_FORMATS; the checksum/structure validators
(isValidAadhaar, isValidGstin, isValidIban, luhnValid, verhoeffValid, …) are exported for
use outside the grid too.
{ id: 'pan', meta: { type: 'text', editable: true, cellMeta: { pattern: 'pan' } } }
{ id: 'aadhaar', meta: { type: 'number', editable: true, cellMeta: { pattern: 'aadhaar' } } }number — also honours meta.format ('currency' / 'percent' / Intl.NumberFormatOptions) and meta.locale.
| cellMeta | Type | Effect |
| --- | --- | --- |
| precision | number | Fixed min+max fraction digits. |
| currency | string (ISO 4217) | Currency format, e.g. 'USD', 'INR'. |
| useGrouping | boolean | Thousands separators on/off. |
dateTime — also honours meta.format (Intl.DateTimeFormatOptions) and meta.locale.
| cellMeta | Type | Effect |
| --- | --- | --- |
| variant | 'date' \| 'time' \| 'dateTime' | Picker + display mode. Default 'date'. |
multiSelect
| cellMeta | Type | Effect |
| --- | --- | --- |
| maxChips | number | Max chips before +N more. Default 3. |
| fitChips | boolean | Width-aware: show as many chips as the column width fits, fold the rest into +N more (widen → more chips). maxChips then acts as an upper cap. |
radio
| cellMeta | Type | Effect |
| --- | --- | --- |
| layout | 'vertical' \| 'horizontal' | Radio arrangement. Default 'vertical'. |
hyperlink
| cellMeta | Type | Effect |
| --- | --- | --- |
| target | string | Anchor target. Default '_blank'. |
files — an attachments cell (images, PDFs, any file). The full how-to is
Files columns — images & PDFs below; in short, the value is a
FileRef[], image files thumbnail automatically, and PDFs thumbnail once you wire pdf.js.
| cellMeta | Type | Effect |
| --- | --- | --- |
| preview | boolean | Click-to-preview. Default true (a file needs a url to preview). |
| pdfThumbnail | boolean \| PdfThumbnailRenderer | Render PDFs as an in-cell page-1 thumbnail via pdf.js. true uses the renderer from <BstPdfThumbnailerProvider>; a function is a per-column renderer. Default false. Falls back to the icon with no renderer; thumbnailUrl wins when present. |
| onUpload | (file: File) => FileRef \| Promise<FileRef> | Called for each picked file — upload it and return the stored ref (busy state shown). Without it, the editor keeps a local object URL so preview still works offline. |
| onDelete | (file: FileRef) => void \| Promise<void> | Called before a file is removed (e.g. delete it on the server). |
| accept / multiple | string / boolean | Passed to the file <input>. |
sparkline — value is number[] (or a comma string).
| cellMeta | Type | Effect |
| --- | --- | --- |
| variant | 'line' \| 'area' \| 'bar' | Chart style. Default 'line'. |
| width / height | number | SVG size in px. Default 84 × 22. |
| color | CSS color | Stroke/fill. Default the accent var. |
| min / max | number | Fix the value scale (else auto from data). |
| showValue | boolean | Append the last value as text. |
kpi — value is number or { value, delta?, data? }; number formatting from meta applies.
| cellMeta | Type | Effect |
| --- | --- | --- |
| invertDelta | boolean | Treat a decrease as good (green). |
| deltaPercent | boolean | Render the delta as a percentage. |
| sparkWidth | number | Width of the optional mini-spark (from value.data). |
qr
| cellMeta | Type | Effect |
| --- | --- | --- |
| ecLevel | 'L' \| 'M' \| 'Q' \| 'H' | Error-correction level. Default 'M'. |
| size | number | Rendered px. Default 88. |
| margin | number | Quiet-zone modules. Default 2. |
barcode
| cellMeta | Type | Effect |
| --- | --- | --- |
| height | number | Bar height in px. Default 38. |
| showText | boolean | Print the value under the bars. Default true. |
richText
| cellMeta | Type | Effect |
| --- | --- | --- |
| render | 'text' \| 'html' | 'text' (default) = safe one-line plain preview; 'html' = show the sanitized HTML formatted (bold / lists / …). |
Options (BstOption)
meta.options feeds singleSelect / multiSelect / radio. The value stored is option.value;
everything else is presentation.
meta: {
type: 'singleSelect',
editable: true,
options: [
{ value: 'todo', label: 'To do', color: '#94a3b8' },
{ value: 'wip', label: 'In progress', color: '#f59e0b', icon: <Clock /> },
{ value: 'done', label: 'Done', color: '#22c55e', avatar: '/u/ada.png' },
],
}| Field | Type | Purpose |
| --- | --- | --- |
| value | string | Stored value (required). |
| label | string | Display text (defaults to value). |
| color | CSS color | Swatch dot / badge tint. |
| icon | ReactNode | Leading icon. |
| avatar | string (url) | Small round image. |
| image | string (url) | Free-form image. |
| description | string | Secondary text (rich editors). |
| disabled | boolean | Non-selectable option. |
Files columns — images & PDFs
The files cell (meta.type: 'files') holds one or more attachments. The value is a FileRef[]:
type FileRef = {
name?: string // label; also used to infer the file type from the extension
url?: string // link to the file — needed for click-preview AND PDF thumbnails
thumbnailUrl?: string // OPTIONAL pre-made image (e.g. a server thumbnail) — always wins
contentType?: string // e.g. 'image/png', 'application/pdf' — the most reliable type hint
}What renders, decided per file, in this order:
- a
thumbnailUrl→ a plain<img>(use this for server-generated thumbnails of anything); - else an image (
contentType: image/*, or a.png/.jpg/.gif/.webp/.svg…name) →<img src={url}>; - else a PDF with
pdfThumbnailon and a renderer available → a pdf.js page-1 render; - else the file-type icon + name.
Clicking any file opens a preview overlay (images inline, PDFs in the browser's native viewer).
Image column — zero setup
Images thumbnail automatically. Just give each file a url (or a thumbnailUrl):
const columns = [
{ id: 'photos', accessorKey: 'photos', header: 'Photos', meta: { type: 'files' } },
]
const rows = [
{ id: '1', photos: [{ name: 'logo.png', url: 'https://cdn.example/logo.png', contentType: 'image/png' }] },
]PDF column — page-1 thumbnails (needs pdf.js)
A PDF shows a type icon by default. To render page 1 as a thumbnail: (1) turn on
cellMeta.pdfThumbnail, and (2) provide a pdf.js renderer. The engine never bundles pdf.js —
you own it and its worker, so it fits your bundler and stays out of everyone else's build.
1. Install pdf.js
npm install pdfjs-dist2. Create a renderer once and wrap your tables in the provider (works with any adapter):
import * as pdfjs from 'pdfjs-dist'
import PdfWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker' // Vite (other bundlers below)
import { BstPdfThumbnailerProvider, createPdfjsThumbnailer } from '@bloomskill/table-engine'
pdfjs.GlobalWorkerOptions.workerPort = new PdfWorker()
const pdfThumbs = createPdfjsThumbnailer(pdfjs)
export function App() {
return (
<BstPdfThumbnailerProvider renderer={pdfThumbs}>
<BstTableMui data={rows} columns={columns} /> {/* or BstTableShadcn / <BstTable> */}
</BstPdfThumbnailerProvider>
)
}3. Turn the thumbnail on for the column (each PDF needs a url):
const columns = [
{
id: 'docs', accessorKey: 'docs', header: 'Docs',
meta: { type: 'files', cellMeta: { pdfThumbnail: true } },
},
]
const rows = [
{ id: '1', docs: [{ name: 'invoice.pdf', url: 'https://files.example/invoice.pdf', contentType: 'application/pdf' }] },
]Done — in the same column, images still thumbnail as <img> and PDFs render page 1. A file with a
thumbnailUrl skips pdf.js entirely. Without a provider, pdfThumbnail: true is harmless (the PDF
keeps its icon).
Notes
- Worker setup by bundler. Vite:
?worker(above). webpack 5 / Next.js:pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(). - Lazy-load pdf.js (keep it out of your initial bundle): pass a loader —
createPdfjsThumbnailer(() => import('pdfjs-dist'))— so pdf.js is fetched when the first thumbnail renders. - Per-column renderer (instead of the provider):
cellMeta.pdfThumbnail: pdfThumbs. createPdfjsThumbnailer(pdfjs, opts?)options:scale(crispness, default 1.5),cache(default true).- Upload / delete files with
cellMeta.onUpload/onDelete(see thefilesrow in cellMeta by cell type); the full-size preview overlay is exported asBstFilePreview.
Options reference
All options for useBstTable(options), grouped. Types shown are the engine's; a boolean | {…}
means passing the object implies enabled.
Core
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| data | TData[] | — | Row data (keep the reference stable between renders). |
| columns | BstTableColumn<TData>[] | — | Column definitions. |
| getRowId | (row, index) => string | index | Stable row identity — required for editing/selection, recommended always. |
| initialState | object | — | Extra TanStack initial state (sorting, columnFilters, grouping, …). |
Data operations
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableSorting | boolean | true | Column sorting (v9). |
| enableGlobalFilter | boolean | true | Global search. |
| enableColumnFilters | boolean | true | Per-column filtering / filter builder. |
| enableSetFilter | boolean | false | Set Filter (X4) — a distinct-values checklist per column in the filter row. Needs enableColumnFilters + enableColumnFilterRow. |
| enableMultiFilter | boolean | false | Multi-filter (X11) — a column with an array meta.filter (e.g. ['condition','set']) stacks those filters (AND). Needs enableColumnFilters + enableColumnFilterRow. |
| enableGrouping | boolean | false | Multi-column grouping + aggregates. |
| pagination | boolean \| { pageSize?: number } | true (10) | Pagination; false shows all rows. |
Columns & layout
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableHiding | boolean | true | Column show/hide. |
| enableColumnResizing | boolean | true | Column resizing. |
| enableColumnPinning | boolean | false | Sticky column pinning ({ start, end }). |
| enableColumnOrdering | boolean | false | Column reorder (menu + header drag). |
| fitColumns | boolean | false | Fit all columns to the viewport — no horizontal scroll (G3). |
| enableResponsive | boolean | false | Hide lowest-priority columns when narrow (G4). No-op under fitColumns. |
| enableStickyHeader | boolean \| { maxHeight?: number \| string; maxRows?: number } | false | Cap the body to a bounded height so rows scroll under a pinned header + filter row (G3/G4). maxHeight = px number / CSS length; maxRows = approx row count; default 440px. Skipped when enableVirtualization is on (it already does this). |
| enableColumnFilterRow | boolean | false | Per-column filter inputs under the header. |
| enableRowNumbers | boolean | false | Row-number column (X9) — a leading # column numbering the current view (continuous across pages; reflects sort + filter). Non-interactive; pinned sticky-left by default (stays leftmost even when other columns are pinned); stays out of sort / filter / columns menu / export. |
| rowNumberHeader | ReactNode | '#' | Header for the row-number column. |
| enableAutoColumns | boolean | false | Auto-generate columns (X27) — infer columns from the data when columns is empty. Ignored once columns is non-empty. |
| autoColumns | AutoColumnsOptions | — | Tune auto-generation: { sampleRows?, include?, exclude?, header?, inferType? }. |
Rows
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableRowSelection | boolean | false | Row-selection checkbox column (v9). |
| enableRowActions | boolean | false | Row add/delete/duplicate. |
| enableExpanding | boolean | false | Master-detail (A4). |
| renderDetail | (row) => ReactNode | — | Detail-panel content (needs enableExpanding). |
| getRowCanExpand | (row) => boolean | all rows | Which rows can expand. |
| enableRowPinning | boolean | false | Freeze rows top/bottom (G1). |
| enableRowResize | boolean | false | Drag a row's bottom edge to set its height (G2). |
| enableAutoRowHeight | boolean | false | Auto row height (X26) — cells wrap and rows grow to fit content; per-column via meta.wrapText. |
| enableVirtualization | boolean \| VirtualizationOptions | false | Row virtualization (D1) — window visible rows for large data. Object tunes overscan / estimateRowSize / estimateColumnSize. |
| enableColumnVirtualization | boolean | false | Also window columns (needs enableVirtualization). |
| onReachEnd | () => void | — | Infinite scroll (A2) — fires near the end of a virtualized body. |
| endReachedThreshold | number | 8 | Rows-from-end that trigger onReachEnd. |
| createRow | () => Partial<TData> | — | Blank-row factory for Add row. |
| tempIdPrefix | string | 'tmp_' | Prefix for created/duplicated row ids. |
Editing, validation & saving
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableEditing | boolean \| { mode?: 'cell'\|'row'\|'batch'; saveOn?; policy? } | false | Inline editing. mode: 'batch' → drafts + one onSave. |
| enableValidation | boolean \| { policy? } | false | Validation feature. |
| enableBatchEditing | boolean | follows enableEditing.mode | Runtime switch: true forces batch, false forces per-cell. The flag the settings sheet toggles. |
| enableUndoRedo | boolean | false | Undo/redo of committed changes. Needs onDataChange. |
| cellTypes | CellTypeRegistry | neutral defaults | Cell-type registry (adapters pass a preset). |
| onDataChange | (next: TData[]) => void | — | Controlled write-back on edit/add/delete/duplicate. |
| onCellCommit | (change: BstCellEdit<TData>) => void | — | Per-cell commit hook — the inline counterpart to onSave. Fires once per committed cell (Enter/blur in cell mode, or per pasted cell) with the single old → new edit. |
| enableEditLog | boolean | false | Debug: console.log each committed cell edit when no onCellCommit. Needs enableEditing. |
| onSave | (event: BstSaveEvent) => void \| BstSaveResult \| Promise<…> | — | Batched save hook — one call per save action. Rejecting keeps every draft; returning a BstSaveResult reconciles the server's response — apply authoritative values + flag partial failures (I4). |
Selection & access control
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableCellSelection | boolean | false | Cell/range selection + keyboard nav. |
| enableTypeToEdit | boolean | false | Type-to-edit: type on a selected cell to overwrite; Enter/Tab commit & move. Needs enableEditing + enableCellSelection. |
| enableClipboard | boolean | false | Copy/paste. Implies enableCellSelection; paste needs enableEditing. |
| enableCopyColumn / enableCopyRow | boolean | true | Sub-toggles of clipboard for whole-column / whole-row copy. |
| enableContextMenu | boolean | false | Right-click menu (X6) — Copy / Export / Autosize defaults; customize via getContextMenuItems. |
| enableFind | boolean \| BstFindOptions | false | Find (X8) — search box that highlights matches + jumps between them (⌘/Ctrl+F · Enter/F3 cycle · Esc close), without hiding rows. Object form: { caseSensitive?, scope?: 'view' \| 'all' }. |
| disabled | boolean | false | Disable the whole grid (F1). |
| rowDisabled | (row) => boolean | — | Disable interaction per row (F2). |
| cellDisabled | ({ row, rowId, columnId }) => boolean | — | Disable interaction per cell (F4). |
Export (X1–X3)
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableExport | boolean \| BstExportOptions | false | Export as CSV / Excel / print. Object form: { csv?, excel?, print?, fileName?, scope?, includeHeaders? } (an object implies enabled). |
| enableCsvExport / enableExcelExport / enablePrint | boolean | true | Per-format sub-toggles of enableExport (also the settings-sheet switches). |
Loading / error overlays (X23)
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableOverlays | boolean | true | Loading / error overlays — paint an overlay while loading or on error. Set false to manage those states yourself. |
| loading | boolean | — | Show the loading overlay. Usually wired from useBstDataSource(...).loading. |
| error | ReactNode \| Error \| null | — | Show the error overlay (error wins over loading). Usually useBstDataSource(...).error. |
| overlayText | { loading?: string; error?: string } | — | Override the default labels ("Loading…" / the error message). |
| renderLoadingOverlay | () => ReactNode | — | Fully custom loading overlay. |
| renderErrorOverlay | (error) => ReactNode | — | Fully custom error overlay. |
Cells & styling
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| enableCellSpanning | boolean | false | Merge cells across columns/rows (A5). |
| getCellSpan | (ctx) => { colSpan?, rowSpan? } | — | Explicit span for an origin cell. |
| conditionalFormats | BstFormatRule[] | — | Conditional formatting rules (K3/F5). |
| enableConditionalFormatting | boolean | true | Runtime off-switch for conditionalFormats. |
| classNames | BstClassNames<TData> | — | Custom class names per structural slot. |
| styles | BstStyles<TData> | — | Inline styles / CSS vars per structural slot. |
Server mode (usually supplied by useBstDataSource)
| Option | Type | Description |
| --- | --- | --- |
| manualSorting / manualFiltering / manualPagination / manualGrouping | boolean | Run that operation server-side. |
| rowCount / pageCount | number | Total rows / pages (drives the page count). |
| autoResetPageIndex | boolean | Whether to jump back to page 0 when the data or filters change. useBstDataSource sets this for you — turn it off to keep the user's page across a refetch. |
| state | object | Controlled { sorting, columnFilters, globalFilter, pagination }. |
| on*Change | (updater) => void | Controlled-state callbacks — onSortingChange · onColumnFiltersChange · onGlobalFilterChange · onPaginationChange · onGroupingChange · onExpandedChange. |
Built-in sortFns: basic · alphanumeric · datetime · text · alphanumericCaseSensitive ·
textCaseSensitive. Built-in filterFns: includesString · inNumberRange · bstCondition (the
operator-aware default).
Editing and validation
▶ Run it live — editable demo, no install.
Use it. Editing is opt-in and controlled: pass enableEditing + an onDataChange handler that
owns data. Columns opt in per-column with meta.editable and pick an editor with meta.type.
const columns: BstTableColumn<Task>[] = [
{ id: 'title', accessorKey: 'title', header: 'Title',
meta: { type: 'text', editable: true, cellMeta: { required: true } } },
{ id: 'points', accessorKey: 'points', header: 'Points',
meta: { type: 'number', editable: true } },
{ id: 'status', accessorKey: 'status', header: 'Status',
meta: { type: 'singleSelect', editable: true, options: [
{ value: 'todo', label: 'To do' }, { value: 'done', label: 'Done', color: '#22c55e' },
] } },
{ id: 'actions', header: '', meta: { type: 'action', actions: { edit: true, delete: true, duplicate: true } } },
]
const table = useBstTable<Task>({
data: rows, columns, getRowId: (r) => r.id,
onDataChange: setRows, // write-back by rowId lands here
enableEditing: true, // double-click a cell; Enter commits, Esc cancels
enableValidation: true,
enableRowActions: true,
createRow: () => ({ title: '', points: 0, status: 'todo' }),
})Customize.
- Editing mode —
enableEditing: { mode }:'cell'(default) — each cell commits on its own.'row'— the action column's Edit → Save defers every cell edit until the row is saved (C2 ≡ I2).'batch'— every edit stays a draft until an explicit save → see Batch editing.
- Save timing —
saveOn: 'enter' | 'blur' | 'explicit'(array allowed). Default['enter','blur']. - Type-to-edit —
enableTypeToEdit(needsenableCellSelection) turns on spreadsheet-style entry: type on a selected cell to overwrite it (the first keystroke seeds the editor via the cell type'sparse, so text and number cells work), then Enter commits and moves down (⇧ up) and Tab commits and moves right (⇧ left). Non-text editors (dropdowns, date pickers) and keys the cell type can't represent are left to double-click / F2. - Per-cell change hook — pass
onCellCommit(change)to be notified the moment each cell commits (edit + click out, or paste) with the single old → newBstCellEdit— the inline counterpart to the batchonSave. For a zero-wiring debug trace, setenableEditLogtoconsole.logevery commit. - Invalid-commit policy —
policy: 'blockCommitOnError'(keep the value a dirty draft, default) or'commitButFlag'(write it, but flag it). - Validation runs in order: the cell-type validator → the
requiredcheck (cellMeta.required) → yourmeta.validate(value, ctx). Cross-column rules read siblings viactx.getSiblingValue(id); return aPromise<FieldError[]>for async (last-write-wins, superseded runs abort viactx.signal). - Popup vs inline — a cell type can default to a dialog editor; force it per column with
meta.editMode: 'popup' | 'inline'. - Programmatic control — use
useBstGrid(options)→{ table, runtime, registry }forruntime.commitAll(),getDirtyChanges(),addRow(), etc. - Portalled editors — if your editor opens an overlay outside the cell (a
Selectmenu, a date-picker popper), setoverlayEditor: trueon theCellTypeso opening it doesn't trigger commit-on-blur; the editor then self-commits (commit(v)on change /commit()on close).
Adapters ship richer editors for these types via createMuiPreset() / createShadcnPreset().
Batch editing and single-call save
Use it. When per-cell writes are too chatty for your backend, put the grid in batch mode:
every edit — typed or pasted — stays an unsaved draft, and nothing reaches onDataChange until an
explicit save. That save announces the whole batch through onSave exactly once.
const { table, runtime } = useBstGrid<Task>({
data: rows, columns, getRowId: (r) => r.id,
enableEditing: { mode: 'batch' }, // every commit defers to a draft
onDataChange: setRows, // applied AFTER onSave resolves
onSave: async ({ changes, rows, next }) => {
// ONE request, your pick of granularity:
// changes → [{ rowId, columnId, field, oldValue, newValue, oldText, newText }]
// rows[].patch → { field: newValue } per changed row (ready for a bulk PATCH)
// next → the full next data array
await api.batchUpdate(rows.map((r) => ({ id: r.rowId, ...r.patch })))
},
})Customize.
- Review before saving —
runtime.getChangeSet()lists every pending edit asoldValue/newValue- formatted
oldText/newText. Adapters render this as the review-changes sheet.
- formatted
- Revert —
runtime.revertCell(rowId, columnId)·runtime.revertRow(rowId)·runtime.cancelAll(). - Save —
runtime.commitAll()validates (blocking errors abort underblockCommitOnError), callsonSaveonce, then writes throughonDataChange. A thrown/rejectedonSavewrites nothing and keeps every draft — the user fixes connectivity and retries. - Row mode too —
onSavealso fires (with just that row's changes) when amode: 'row'session saves, so both deferred modes share one contract. Plain cell mode has no drafts → useonDataChange. - Let end-users switch it —
enableBatchEditingoverrides the mode at runtime; it's the toggle the settings sheet exposes under Editing → Batch editing.
Reconcile the server's response (I4). onSave may return a BstSaveResult to fold the
backend's answer back into the grid — think Promise.allSettled, not resolve-vs-reject: throwing
still aborts the whole save, while returning settles each row.
onSave: async ({ rows }) => {
const res = await api.batchUpdate(rows.map((r) => ({ id: r.rowId, ...r.patch })))
return {
// accepted rows — the grid adopts the server's OFFICIAL values (ids, timestamps,
// recomputed/normalised fields), not what was typed; these drafts clear:
applied: res.saved.map((r) => ({ rowId: r.id, values: r })),
// rejected rows/cells — draft KEPT + error shown (omit columnId for a whole-row
// failure); every other changed row still commits:
failed: res.errors.map((e) => ({ rowId: e.id, columnId: e.field, error: e.message })),
}
}- Return nothing → the default: every draft commits with the typed value (backward-compatible).
applied→ those rows adopt the server'svalues(a full or partial row, merged), so the grid shows what was actually stored — no drift. A new row's temporary id maps to the real id here.failed→ those rows/cells keep their draft and light up with theerror(the validation-error UI).commitAll()resolvesfalsewhen anything failed, so the review sheet stays open on them.
Selection, keyboard and clipboard
Use it. Two opt-in flags add Excel-like productivity through the same neutral <BstTable/> body,
so every adapter inherits them.
const table = useBstTable<Task>({
data: rows, columns, getRowId: (r) => r.id, onDataChange: setRows,
enableEditing: true, // paste writes through the editing lifecycle
enableCellSelection: true, // click / keyboard selection
enableClipboard: true, // copy + paste (implies enableCellSelection)
})- Select — click a cell; Shift-click selects a rectangle.
- Navigate — Arrow moves; Shift+Arrow grows the range; Tab steps and wraps to the next row; Home / End to the row edges (Ctrl/Cmd+Home / End to grid corners); Ctrl/Cmd+A all.
- Edit — Enter / F2 opens the editor on an editable cell; Esc clears.
- Copy — Ctrl/Cmd+C copies the selection as TSV (each value formatted by its cell type).
- Paste — Ctrl/Cmd+V writes TSV from the active cell across as many rows/columns as it spans;
read-only cells are skipped, values parsed + validated. Paste requires
enableEditing.
Customize.
Whole-column copy (H3) — Ctrl/Cmd+Space (or
runtime.copyColumn(id)) grabs every row across all pages (pre-pagination, in filter+sort order). Gate it withenableCopyColumn(defaulttrue).Whole-row copy (H2) — Shift+Space /
runtime.copyRow(id). Gate withenableCopyRow(defaulttrue).Drive it programmatically — via
useBstGrid→runtime.setActiveCell/moveActive/getSelectionMatrix/copySelection/pasteFromText.runtime.getSelectionStats()returns{ count, numericCount, sum, avg, min, max }over the selection — the X5 status bar (showStatusBarin the adapters) renders it.Right-click menu (X6) —
enableContextMenuopens a menu at the cursor with Copy · Copy row · Copy column (while clipboard is on), Export CSV / Excel (while export is on) and Autosize column. Reshape it withgetContextMenuItems(ctx) => BstContextMenuItem[]— spreadctx.defaultItemsand add your own (each{ label, onSelect, disabled?, separator?, icon? }). Both adapters inherit it; if the resolved list is empty the native browser menu is left alone.Shortcuts overlay —
showShortcuts(in the adapters) adds a "?" button + a dep-free overlay listing the shortcuts active on this grid (also opens on?). Headless:<BstShortcuts table={table} />BST_SHORTCUTS_REGISTRY+ the pureresolveActiveShortcuts(flags, query)/formatShortcutToken.
Cell notes / comments (
enableNotes) — attach annotations to individual cells without mutating row data. Cells with notes display a red corner dog-ear marker; hovering over the cell opens a preview popover. Press Shift+F2 (or click the marker / right-click context menu "Add note" / "Edit note") to open the built-in resizable note editor. Supportsnotes(initial or controlled map),onNotesChange(notes),onNoteSave({ rowId, columnId, note, prevNote }), and column-levelmeta: { notesAllowed: false }.
Selection lives in the interaction store (not
table.setState) and is materialised at paint from the active/anchor cell ids — moving the cursor re-renders only the cells whose state changed, never the whole grid.
Find
Use it. enableFind adds an in-grid find bar that highlights every match and jumps between
them — the browser-Ctrl+F experience inside the grid. It is distinct from global search
(enableGlobalFilter), which hides non-matching rows: Find hides nothing, so you keep the
surrounding context while locating a value.
- Open with ⌘/Ctrl+F (or the adapters' toolbar ⌕ Find button,
showFind). Cycle with Enter / Shift+Enter in the box or F3 / Shift+F3; close with Esc. A "n / m" counter shows the current position. - What matches — the cell's display text (the same
formatValueused by copy/export), so "what you find" equals "what you copy". Plain-text cells get in-place<mark>highlights; every matched cell (including non-text ones) gets a tint, and the current match is outlined and scrolled into view. - Configure via the object form:
enableFind={{ caseSensitive: true, scope: 'all' }}—scope: 'view'(default) searches the current page/window,'all'searches every filtered row. - Drive it programmatically —
runtime.openFind()/closeFind()/setFindQuery(q)/findNext()/findPrev().
Works independently of
enableCellSelection. Match state lives in the interaction store and is painted per-cell, so typing / stepping re-renders only the matched cells, never the whole grid.
Export (CSV / Excel / print)
Use it. enableExport adds a toolbar Export menu — download CSV, download Excel
(.xlsx) or print — and the programmatic API runtime.exportCsv() / exportExcel() /
printTable(). Values are formatted per cell type (so the file matches what the grid shows and
what copy produces), and the default scope is every filtered + sorted row across all pages
(pre-pagination), not just the visible page.
const table = useBstTable<Row>({
data, columns, getRowId: (r) => r.id,
enableExport: { fileName: 'people' }, // or `enableExport: true`
})Customize. Pass a BstExportOptions object (an object implies enabled, §12):
| Field | Type | Default | Effect |
| --- | --- | --- | --- |
| csv / excel / print | boolean | true | Which formats the menu offers. Also settable at the top level as enableCsvExport / enableExcelExport / enablePrint — the settings-sheet switches, which win. |
| fileName | string | 'export' | Base download name; the extension is added per format. |
| scope | 'all' \| 'page' | 'all' | Export every page (pre-pagination) or just the current page. |
| includeHeaders | boolean | true | Write the header row. |
- Dependency-free. CSV is RFC-4180 (quoted fields, UTF-8 BOM so Excel reads it). The
.xlsxis a real OOXML package built here from a store-only ZIP + SpreadsheetML — noexceljs/sheetjs;numbercells are written as typed numeric cells. Print opens a standalone, styled table view. - Per-call overrides. Each
runtime.export*accepts{ scope?, fileName?, includeHeaders?, delimiter? }(e.g.runtime.exportCsv({ scope: 'page', delimiter: ';' })). - Serializers are exported for custom flows:
toCsv,toXlsx,buildPrintHtml, plusdownloadBlob/printHtml(both no-op under SSR). - Adapters render the menu via
showExport(default: followsenableExport); action columns and grouped/aggregate rows are skipped automatically.
Access control
Use it. Interactivity resolves through one grid → row → column → cell cascade — the first
disable in the chain wins. A disabled cell greys out and can't be edited (it overrides
meta.editable), but stays selectable/copyable.
useBstTable<Row>({
data, columns, getRowId: (r) => r.id,
enableEditing: true,
disabled: readOnlyMode, // F1 — whole grid
rowDisabled: (row) => row.status === 'locked', // F2 — a row
cellDisabled: ({ row, columnId }) => // F4 — any cell, cross-cutting
columnId === 'salary' && !currentUser.canSeePay,
})
// F3 (whole column) / F4 (per-row) live on the column:
{ id: 'score', accessorKey: 'score',
meta: { type: 'number', editable: true, disabled: (row) => row.final } }Customize. Read the resolved state via runtime.getCellAccess(rowId, columnId) →
{ disabled, editable } (also runtime.isCellDisabled / isRowDisabled). A runtime per-column edit
lock — runtime.setColumnEditable(columnId, on) — overrides meta.editable live; adapters surface it
as a lock/unlock toggle in the Columns menu (showColumnEditToggle).
Row selection
Use it. enableRowSelection adds a leading checkbox column — header "select all" (indeterminate on
a partial selection), per-row checkboxes, and a selected-row highlight. Built on v9's
rowSelectionFeature, so state lives in table.state.rowSelection keyed by getRowId.
const table = useBstTable<Row>({ data, columns, getRowId: (r) => r.id, enableRowSelection: true })
const chosen = table.getSelectedRowModel().rows.map((r) => r.original) // read it
table.resetRowSelection() // clear itCustomize. The checkbox column sits outside the column model, so it never joins cell-selection
ranges, copy output, resizing or reordering. Adapters add a "{n} selected" chip + Clear to the
toolbar (showSelectionInfo, defaults to follow enableRowSelection).
Undo and redo
Use it. enableUndoRedo snapshots data before every committed change (edits, paste, row
add/delete/duplicate), so it needs a controlled onDataChange.
const { table, runtime } = useBstGrid<Row>({
data, columns, getRowId: (r) => r.id, onDataChange: setRows,
enableEditing: true,
enableUndoRedo: true,
})
// keyboard: Ctrl/Cmd+Z undo · Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y redo — or drive it:
runtime.undo(); runtime.redo(); runtime.canUndo(); runtime.canRedo()Customize. A fresh change clears the redo stack. Adapters render Undo/Redo buttons (showUndoRedo).
Filtering
Three layers, all optional and composable:
- Global search (
enableGlobalFilter, default on) — one box filters across columns. Adapters render it (showSearch). - Column filters (
enableColumnFilters, default on) — every column defaults to the operator-awarebstConditionfilterFn, which reads a{ op, value }condition. - Filter builder / filter row — neutral UIs over those conditions.
Use it.
import { useBstTable, BstTable, BstFilterBuilder } from '@bloomskill/table-engine'
const table = useBstTable({ data, columns, getRowId: (r) => r.id })
return (
<div className="bst-table-root">
<BstFilterBuilder table={table} /> {/* adapters wrap this in a "Filters" panel */}
<BstTable table={table} />
</div>
)Customize.
Operators are chosen by
meta.type— text (contains / equals / starts with / is empty…), number (= ≠ > < between), date (on / after / before / between), select (is / is not), boolean.Per-column filter row —
enableColumnFilterRowrenders a type-aware input under each header (the "dual filter"; coexists with the builder panel).Set Filter (X4) —
enableSetFilterturns the filter-row control for categorical columns (singleSelect/multiSelect/radio/boolean) into an Excel-style checklist of distinct values (BstSetFilter: search · select-all / clear · per-value counts · a "(Blanks)" bucket). Force it on any column withmeta.filter: 'set', or off withmeta.filter: 'condition'. It writes an{ op: 'set' }condition, so it composes with the builder andbstCondition. NeedsenableColumnFiltersand the filter row visible (enableColumnFilterRow). The list is faceted: it offers the values found in the rows that pass every other column's filter, so it narrows as you filter elsewhere (filter Dept toEngand Plan stops offering values no Eng row has). A column's own selection is excluded from that calculation — otherwise ticking one value would hide the rest and you could never widen the selection again. Built on v9'scolumnFacetingFeature.Multi-filter (X11) —
enableMultiFilterlets a column stack several filter types. Set itsmeta.filterto an array (e.g.['condition', 'set']) and the filter row shows those filters stacked; a row must satisfy all of them (AND). Stored as a compound{ op: 'and', conditions }value the samebstConditionfilterFn understands (helperscombineFilterConditions+ typeFilterConditionGroup). A'set'part also needsenableSetFilter; whenenableMultiFilteris off, an arraymeta.filterfalls back to its first entry.{ id: 'name', accessorKey: 'name', header: 'Name', meta: { type: 'text', filter: ['condition', 'set'] } } // "contains" input + distinct-values checklistCompose your own — the exported
evalCondition,operatorsForType, and*_OPERATORStables let you build custom filter UIs. Read active filters viatable.state.columnFilters.
Sorting
On by default (enableSorting). Set a column's sortFn to a built-in (basic · alphanumeric ·
datetime · text) or a custom function; multi-sort is supported (Shift-click headers in the
adapters). The default fns fold case — alphanumericCaseSensitive and textCaseSensitive order
uppercase before lowercase instead.
Pagination
On by default (10/page). pagination={{ pageSize: 25 }} sets the size; pagination={false} shows all
rows. Adapters render the bar (showPagination) with pageSizeOptions (default [5,10,20,50]).
Grouping and aggregation
Use it. enableGrouping groups rows into collapsible group headers with per-column aggregates.
const columns: BstTableColumn<Sale>[] = [
{ id: 'region', accessorKey: 'region', header: 'Region' },
{ id: 'amount', accessorKey: 'amount', header: 'Amount',
aggregationFn: 'sum', meta: { type: 'number', cellMeta: { currency: 'USD' } } },
]
const table = useBstTable<Sale>({
data, columns, getRowId: (r) => r.id,
enableGrouping: true,
initialState: { grouping: ['region'] }, // or table.setGrouping([...])
})Customize. A column aggregates by declaring aggregationFn: 'sum' | 'count' | 'mean' | 'median' |
'min' | 'max' | 'extent' | 'first' | 'last' | 'unique' | 'uniqueCount'. Prefer median over mean
on a skewed column (salary, duration) where one extreme row drags the average; first/last surface
one row's value for a column that is constant within the group, and unique lists the distinct values
where uniqueCount only counts them. Adapters add a group toggle (▤) in the columns menu. Built on v9's
columnGroupingFeature + rowAggregationFeature. (Client mode — at 1M rows group server-side.)
Column layout
All the ways to arrange columns. Resize + hide are on by default; the rest are opt-in.
| Want | Turn on | Notes |
| --- | --- | --- |
| Show / hide | enableHiding (on) | Adapters render a columns menu (showColumnsMenu). |
| Resize | enableColumnResizing (on) | Drag the header edge (table-layout: fixed). |
| Auto-size | — | Double-click a resize handle to fit content (sampled canvas.measureText, clamped to min/maxSize). computeAutoWidth is exported. |
| Pin (freeze) | enableColumnPinning | Sticky start/end columns, kept fully opaque. |
| Reorder | enableColumnOrdering | Column-menu move + header drag-drop. |
| Fit to viewport | fitColumns | No horizontal scroll — data columns share the width; utility columns stay fixed; manual resize suppressed while on. |
| Responsive hide | enableResponsive | Hides lowest-meta.responsivePriority columns when narrow, restores as it widens (ResizeObserver). No-op under fitColumns. |
Adapters add a density toggle (showDensityToggle → data-bst-density → compact / comfortable rows).
Row layout
| Want | Turn on | Notes |
| --- | --- | --- |
| Master-detail | enableExpanding + renderDetail(row) | Leading expander column; clicking opens a full-width detail panel. getRowCanExpand(row) gates which rows expand. |
| Row pinning | enableRowPinning | Leading pin column; the toggle cycles a row top → bottom → unpinned. Pinned rows survive sort/filter/pagination and stick while the body scrolls. |
| Row resizing | enableRowResize | Drag any row's bottom edge to set its height (min 24px; double-click resets). Heights are local UI state. |
| Auto row height | enableAutoRowHeight | Body cells wrap and each row grows to fit its content — browser-measured, no JS. Opt a single column in with meta.wrapText. A manually-resized row keeps its set height and clips the wrapped content (X26). |
| Sticky-header viewport | enableStickyHeader | Caps the scroll box to a bounded height so rows scroll under a sticky header + filter row instead of the table growing taller as the page size grows (G3/G4). true → 440px; { maxHeight: 500 } / { maxHeight: '60vh' } / { maxRows: 10 } to size it. Already included in enableVirtualization, so the standalone class is skipped when windowing is on. Height overridable via styles.root / the --bst-max-height var. |
useBstTable<Order>({
data, columns, getRowId: (r) => r.id,
enableExpanding: true,
getRowCanExpand: (row) => row.lineItems.length > 0,
renderDetail: (row) => <OrderLines items={row.lineItems} />,
})Row numbers, auto-columns & overlays
Three zero-config conveniences, each a §12 toggle (all appear in the runtime settings sheet):
Row numbers (X9) — enableRowNumbers prepends a leading, non-interactive # column that
numbers the current view: numbering is continuous across pages and follows the active sort +
filter (not the raw data order). It never sorts, filters, hides, resizes or reorders, so it stays out
of the columns menu, filter row and export. It is pinned to the start (sticky-left) by default, so
it stays the leftmost column even when you pin another column, and stays visible during horizontal
scroll. Override the
