npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bloomskill/table-engine

v0.41.0

Published

Bst-Table headless engine — TanStack Table v9 wiring + neutral renderer + CSS-var theme tokens. UI-library agnostic.

Readme

@bloomskill/table-engine

npm license

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-dom are 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).

Contents

Start here  · Install  · Quick start  · Live examples  · Feature map

Columnshow 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 guidesuse 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

Open in StackBlitz — live and editable, 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. BstTable renders 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 returned table instance. .bst-table-root applies the theme variables.


Live examples

Each example imports the published package from npm — the same code you'd write. Source lives in examples/. Open one in StackBlitz (runs Vite in your browser — instant preview), or run it locally:

cd examples/quick-start   # or editing · cell-types · conditional-formatting · cell-spanning · server-mode · field-formats
npm install && npm run dev

| Example | Shows | Run it | | --- | --- | --- | | Quick start | Sortable + paginated grid | Open in StackBlitz | | Editing & validation | Inline editing · validation · selects · row actions | Open in StackBlitz | | Cell types | sparkline · KPI · badges · chips · boolean · barcode | Open in StackBlitz | | Conditional formatting | Value-driven cell/row colours | Open in StackBlitz | | Cell spanning | Merged cells via meta.rowSpan: 'group' | Open in StackBlitz | | Server mode | useBstDataSource — server-style sort/filter/paginate over 5k rows | Open in StackBlitz | | Field formats | ERP validation + masks via cellMeta.pattern — Aadhaar · PAN · GSTIN · IBAN · card | Open in StackBlitz |

StackBlitz builds the project on open; a brief blank preview just means it's still installing. Running locally (above) is always instant and is the guaranteed path.


Feature 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 | | 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 |

⌨️ 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 (AG21) | 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 under meta.

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 on StackBlitz.

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 + defineCellType and pass them as cellTypes (adapters start from createMuiPreset() / 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:

  1. a thumbnailUrl → a plain <img> (use this for server-generated thumbnails of anything);
  2. else an image (contentType: image/*, or a .png/.jpg/.gif/.webp/.svg… name) → <img src={url}>;
  3. else a PDF with pdfThumbnail on and a renderer available → a pdf.js page-1 render;
  4. 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-dist

2. 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 the files row in cellMeta by cell type); the full-size preview overlay is exported as BstFilePreview.

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 (AG4) — a distinct-values checklist per column in the filter row. Needs enableColumnFilters + enableColumnFilterRow. | | enableMultiFilter | boolean | false | Multi-filter (AG11) — 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 (AG9) — 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 (AG27) — 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 (AG26) — 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. | | onSave | (event: BstSaveEvent) => void \| Promise<void> | — | Batched save hook — one call per save action. Rejecting keeps every draft. |

Selection & access control

| Option | Type | Default | Description | | --- | --- | --- | --- | | enableCellSelection | boolean | false | Cell/range selection + keyboard nav. | | 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 (AG6) — Copy / Export / Autosize defaults; customize via getContextMenuItems. | | 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 (AG1–AG3)

| 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 (AG23)

| 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. Built-in filterFns: includesString · inNumberRange · bstCondition (the operator-aware default).


Editing and validation

Run it live on StackBlitz.

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 modeenableEditing: { 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 timingsaveOn: 'enter' | 'blur' | 'explicit' (array allowed). Default ['enter','blur'].
  • Invalid-commit policypolicy: 'blockCommitOnError' (keep the value a dirty draft, default) or 'commitButFlag' (write it, but flag it).
  • Validation runs in order: the cell-type validator → the required check (cellMeta.required) → your meta.validate(value, ctx). Cross-column rules read siblings via ctx.getSiblingValue(id); return a Promise<FieldError[]> for async (last-write-wins, superseded runs abort via ctx.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 } for runtime.commitAll(), getDirtyChanges(), addRow(), etc.
  • Portalled editors — if your editor opens an overlay outside the cell (a Select menu, a date-picker popper), set overlayEditor: true on the CellType so 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 savingruntime.getChangeSet() lists every pending edit as oldValue/newValue
    • formatted oldText/newText. Adapters render this as the review-changes sheet.
  • Revertruntime.revertCell(rowId, columnId) · runtime.revertRow(rowId) · runtime.cancelAll().
  • Saveruntime.commitAll() validates (blocking errors abort under blockCommitOnError), calls onSave once, then writes through onDataChange. A thrown/rejected onSave writes nothing and keeps every draft — the user fixes connectivity and retries.
  • Row mode tooonSave also fires (with just that row's changes) when a mode: 'row' session saves, so both deferred modes share one contract. Plain cell mode has no drafts → use onDataChange.
  • Let end-users switch itenableBatchEditing overrides the mode at runtime; it's the toggle the settings sheet exposes under Editing → Batch editing.

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.
  • NavigateArrow 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.
  • EditEnter / F2 opens the editor on an editable cell; Esc clears.
  • CopyCtrl/Cmd+C copies the selection as TSV (each value formatted by its cell type).
  • PasteCtrl/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 with enableCopyColumn (default true).
  • Whole-row copy (H2)Shift+Space / runtime.copyRow(id). Gate with enableCopyRow (default true).
  • Drive it programmatically — via useBstGridruntime.setActiveCell / moveActive / getSelectionMatrix / copySelection / pasteFromText. runtime.getSelectionStats() returns { count, numericCount, sum, avg, min, max } over the selection — the AG5 status bar (showStatusBar in the adapters) renders it.
  • Right-click menu (AG6)enableContextMenu opens 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 with getContextMenuItems(ctx) => BstContextMenuItem[] — spread ctx.defaultItems and 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 overlayshowShortcuts (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 pure resolveActiveShortcuts(flags, query) / formatShortcutToken.

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.

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 .xlsx is a real OOXML package built here from a store-only ZIP + SpreadsheetML — no exceljs / sheetjs; number cells 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, plus downloadBlob / printHtml (both no-op under SSR).
  • Adapters render the menu via showExport (default: follows enableExport); 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 lockruntime.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 it

Customize. 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-aware bstCondition filterFn, 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 rowenableColumnFilterRow renders a type-aware input under each header (the "dual filter"; coexists with the builder panel).

  • Set Filter (AG4)enableSetFilter turns 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 with meta.filter: 'set', or off with meta.filter: 'condition'. It writes an { op: 'set' } condition, so it composes with the builder and bstCondition. Needs enableColumnFilters and the filter row visible (enableColumnFilterRow).

  • Multi-filter (AG11)enableMultiFilter lets a column stack several filter types. Set its meta.filter to 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 same bstCondition filterFn understands (helpers combineFilterConditions + type FilterConditionGroup). A 'set' part also needs enableSetFilter; when enableMultiFilter is off, an array meta.filter falls back to its first entry.

    { id: 'name', accessorKey: 'name', header: 'Name',
      meta: { type: 'text', filter: ['condition', 'set'] } } // "contains" input + distinct-values checklist
  • Compose your own — the exported evalCondition, operatorsForType, and *_OPERATORS tables let you build custom filter UIs. Read active filters via table.state.columnFilters.

Sorting

On by default (enableSorting). Set a column's sortFn to a built-in (basic · alphanumeric · datetime) or a custom function; multi-sort is supported (Shift-click headers in the adapters).

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' | 'min' | 'max' | 'extent' | 'uniqueCount'. 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 (showDensityToggledata-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 (AG26). | | 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 (AG9)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 header with rowNumberHeader.

Auto-generate columns (AG27)enableAutoColumns infers columns from the data when you pass no columns (an empty array): one column per key found across a sample of rows, with the cell type guessed (number / boolean / date, else text) and a humanized header (unitPrice → "Unit Price"). Explicit columns always win. The pure helper is exported too:

import { autoGenerateColumns } from '@bloomskill/table-engine'
const columns = autoGenerateColumns(rows, { exclude: ['id'], sampleRows: 100 })
// or just: useBstTable({ data, columns: [], enableAutoColumns: true, getRowId })

Loading / error overlays (AG23)enableOverlays (on by default) paints an overlay over the grid while loading is true or when error is set (error wins). Wire it to a server source and it works for free — useBstDataSource / useBstInfiniteDataSource expose loading + error on their tableProps:

const { tableProps } = useBstDataSource(source)   // carries loading + error
return <BstTableMui data={rows} columns={columns} getRowId={(r) => r.id} {...tableProps} />
// customize: overlayText={{ loading: 'Fetching…' }} / renderErrorOverlay={(e) => <MyError e={e} />}

Conditional formatting

Run it live on StackBlitz.

Use it. conditionalFormats is a declarative array of rules that colour cells/rows by value (K3), or blank a cell (F5). Presence is the opt-in; enableConditionalFormatting (default on) is the runtime off-switch.

const table = useBstTable<Row>({
  data, columns, getRowId: (r) => r.id,
  conditionalFormats: [
    // colour the amount cell red when it's negative
    { scope: 'cell', columnId: 'amount', when: { op: 'lt', value: 0 },
      className: 'text-rose-600', style: { fontWeight: 600 } },
    // tint the whole row when status = overdue
    { scope: 'row', columnId: 'status', when: { op: 'equals', value: 'overdue' },
      style: { background: '#fff1f2' } },
  ],
})

Customize. Rules reuse the filter operators; scope: 'cell' | 'row'; hideContent blanks a cell (F5). They compose with the classNames/styles slots and meta.cellStyle. Build them at runtime with <BstConditionalFormatBuilder> (adapters host it behind a "Formats" button, showFormatBuilder).

Cell spanning

Run it live on StackBlitz.

Use it. enableCellSpanning merges body cells across columns and rows.

useBstTable<Row>({
  data, columns, getRowId: (r) => r.id,
  enableCellSpanning: true,
  // one banner cell spans 3 columns:
  getCellSpan: ({ row, colIndex }) =>
    row.kind === 'banner' && colIndex === 0 ? { colSpan: 3 } : undefined,
})
// columns: [{ id: 'region', meta: { type: 'text', rowSpan: 'group' } }, …]  // auto row-merge

Customize.

  • Auto row-mergemeta.rowSpan: 'group' merges vertically-consecutive equal values in a column.
  • ExplicitgetCellSpan(ctx) returns { colSpan?, rowSpan? } for the top-left origin; covered cells leave the DOM; spans clamp to the grid bounds.
  • It's render-only (no v9 dependency), so it composes with sort/filter/paginate. Because covered cells leave the DOM, prefer spanning for display grids (or keep spanned columns out of cell editing). The planner computeCellSpans is exported.

Custom CSS

Use it. Style the grid with your own classes/styles via slot objects. Each slot composes with the built-in bst-* class (never replaces it), so a theme keeps working underneath.

const table = useBstTable<Row>({
  data, columns, getRowId: (r) => r.id,
  classNames: {
    root: 'ring-1 ring-slate-200 rounded-xl',
    headerCell: 'uppercase tracking-wide',
    row: ({ row }) => (row.status === 'overdue' ? 'bg-rose-50' : undefined), // conditional (K2)
    cell: ({ columnId }) => `col-${columnId}`,
  },
  styles: {
    row: ({ index }) => ({ '--stripe': index % 2 ? '#fafafa' : '#fff' } as React.CSSProperties),
  },
})

Slots.

| Slot | Element | Function form? | | --- | --- | --- | | root | outer scroll wrapper | — | | table / header / headerRow / body | structural | — | | headerCell | <th> | (ctx: { columnId }) => … | | filterRow | per-column filter <tr> | — | | row | body <tr> | (ctx: { row, rowId, index }) => … | | cell | body <td> | (ctx: CellRenderProps) => … | | empty | the "No rows" <td> | — |

Customize. Per-column CSS lives on the column: meta.cellClassName / meta.cellStyle (body cells, K1) and meta.headerClassName / meta.headerStyle (that column's <th>) — the per-column values win over the global styles.cell / styles.headerCell. The theme itself is CSS variables (--bst-table-*) — override them on .bst-table-root. Adapters expose className / style for the outer card.

Body icons

Use it. The grid body renders inline-SVG icons (skin-neutral, never emoji) for the sort indicator, the expander, the row-pin control, the boolean cell, file-type icons, builder remove buttons and the KPI trend chip. Override any slot via the icons prop:

<BstTable table={table} icons={{ pin: MyPinIcon, booleanTrue: MyCheckIcon }} />

Customize. An icon is any React.ComponentType<{ size?: number | string; className?: string }>; unspecified slots keep the built-in SVG. Slots: sortAsc · sortDesc · sortNone · expandExpanded · expandCollapsed · pin · booleanTrue · remove · trendUp · trendDown · fileGeneric · filePdf · fileDoc · fileSheet · fileSlides · fileArchive · fileAudio · fileVideo. The MUI / shadcn adapters forward their own icon set automatically, so the whole grid uses one library. <BstFilterBuilder> / <BstConditionalFormatBuilder> take the same prop.

Runtime settings sheet

📖 Full guide: docs/settings-sheet.md.

Use it. useBstSettings is a headless hook that lets end-users turn a grid's features on/off at runtime (no code change), per table, persisted to localStorage. Adapters render it as a gear → drawer/sheet (showSettings); here's the raw model:

import { useBstSettings } from '@bloomskill/table-engine'

const { props: effective, model } = useBstSettings(props, { persistKey: 'people' })
const table = useBstTable(effective)   // enable*/show* now reflect the user's choices
// render model.groups → switches, each item: { key, label, group, value, set, toggle, reset }

Customize.

  • Which toggles appear — every instance-level boolean toggle, grouped: Data operations · Columns · Rows · Editing · Selection & clipboard · Display. Default-on data features always show; most opt-in features appear only once you've provisioned them (so a user can't switch on something the grid isn't wired for) — but a few end-user escape hatches stay always shown even unprovisioned: row grouping, copy column/row, the per-column filter row, row resize, and row/column virtualization. Pass features: BstSettingKey[] to curate the list.
  • Search — the sheet lists 30+ toggles, so adapters render a search box (highlighted header + a filter input) that narrows the list by label / hint / group name. On by default, it appears only once the sheet has more than a handful of items; search: false hides it, search: true always shows it. The matching is the pure helper filterSettingsGroups(model.groups, query) — both adapters share it.
  • Dependencies — sub-f