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

npm license docs

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

📚 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. 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.


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 dev

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 | | 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 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 — 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 + 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 (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 (needs enableCellSelection) turns on spreadsheet-style entry: type on a selected cell to overwrite it (the first keystroke seeds the editor via the cell type's parse, 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 → new BstCellEdit — the inline counterpart to the batch onSave. For a zero-wiring debug trace, set enableEditLog to console.log every 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 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 saving — runtime.getChangeSet() lists every pending edit as oldValue/newValue
    • formatted oldText/newText. Adapters render this as the review-changes sheet.
  • Revert — runtime.revertCell(rowId, columnId) · runtime.revertRow(rowId) · runtime.cancelAll().
  • Save — runtime.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 too — onSave 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 it — enableBatchEditing overrides 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's values (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 the error (the validation-error UI). commitAll() resolves false when 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 with enableCopyColumn (default true).

  • Whole-row copy (H2) — Shift+Space / runtime.copyRow(id). Gate with enableCopyRow (default true).

  • 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 (showStatusBar in the adapters) renders it.

  • Right-click menu (X6) — 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 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 pure resolveActiveShortcuts(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. Supports notes (initial or controlled map), onNotesChange(notes), onNoteSave({ rowId, columnId, note, prevNote }), and column-level meta: { 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 formatValue used 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 .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 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 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 row — enableColumnFilterRow renders a type-aware input under each header (the "dual filter"; coexists with the builder panel).

  • Set Filter (X4) — 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). 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 to Eng and 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's columnFacetingFeature.

  • Multi-filter (X11) — 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 · 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