bo-grid v3.1.0

Export & API

.md

Props reference

Every Grid prop, column option and grid API method, generated from the TypeScript types. The same data is at /api.json for tools and coding agents.

Grid props

Set on <Grid> (or BoGrid in React and Vue). Required props are starred.

Grid props from the types
rows* GridRow[]

The data: one plain object per row, each with a unique id, a number or a string (or name another key with getRowId).

Example
let rows: GridRow[] = $state([
  { id: 1, sym: 'VNM', last: 68.5 },
  { id: 2, sym: 'FPT', last: 121.3 },
]);
columns* ColumnDef[]

The columns, in display order. A column's type picks how its values show, sort and filter.

Example
const columns: ColumnDef[] = [
  { type: 'text', key: 'sym', header: 'Symbol', width: 90 },
  { type: 'price', key: 'last', header: 'Last', flash: 'auto' },
];
height* number | string

Viewport height. A number is the scroll viewport's pixel height (the grid's total height is that plus header/toolbar/footer chrome). A CSS string (e.g. '100%', '80vh', '480px') sizes the whole grid element and the viewport auto-fits the space left after the chrome — give the grid a sized parent (or use '100%' inside a flex/grid cell).

Example
<Grid {rows} {columns} height="100%" />
rowHeight number | ((row: GridRow, index: number) => number) Default 36

Row height in px (uniform), or a function for variable heights (in-memory mode only).

Example
<Grid {rows} {columns} height={480} rowHeight={(row) => (row.note ? 56 : 36)} />
theme 'dark' | 'light' | GridTheme Default 'dark'

Built-in theme name, or a token map (a preset such as midnightTheme, or your own, spread over one).

Example
<Grid {rows} {columns} height={480} theme={{ ...lightTheme, up: '#0a7f4f' }} />
resizable boolean Default true

Allow drag-to-resize column widths. A drag (or an autosize) changes that column only: the others keep the width they have on screen, flex columns included, so the ones after it move along. Double-click a grip to give that column back its own width; Reset widths in the column menu gives every column back its own. Opt out per column with resizable: false.

cellSelection boolean Default true

Highlight cells/ranges on click-drag (the blue selection fill + focus ring + fill handle). Set false for a read-only/display grid where clicks should pass straight through to onRowClick/onCellClick without a selection highlight.

rowSelection boolean Default false

Show a leading checkbox column for whole-row selection (keyed by row id, stable across sort/filter).

onRowSelectionChange (selectedIds: Array<string | number>) => void

Called with the selected row ids whenever the row-selection set changes.

Example
<Grid {rows} {columns} height={480} rowSelection onRowSelectionChange={(ids) => (selected = ids)} />
hiddenColumns string[] Default []

Column keys to hide (controlled). Build your own column-picker UI and drive this prop — the grid stays presentation-only. Composed (union) with columns the user hides at runtime via the column menu.

Example
<Grid {rows} {columns} height={480} hiddenColumns={showVolume ? [] : ['vol']} />
onColumnVisibilityChange (hidden: string[]) => void

Called with all currently-hidden column keys whenever the runtime set changes (column menu hide/show).

Example
<Grid {rows} {columns} height={480} columnMenu onColumnVisibilityChange={(hidden) => save('hidden', hidden)} />
columnMenu boolean Default false

Enable a per-column header menu (a ⋮ trigger) with sort, hide and (with filterMenu) filter actions.

columnsPanel boolean Default false

Show a "Columns" button that opens a panel to toggle column visibility (the place to restore columns hidden via the menu). Lazy-loaded.

virtualizeColumns boolean Default false

Render only the columns within the horizontal scroll window (+ overscan) for very wide grids (100+ columns). Forces fixed-width horizontal scroll; pinned columns always render. Off, the columns fit the grid's width.

rowClass (row: GridRow) => string | undefined

Return extra CSS class(es) for a data row (e.g. to colour by value). Style them via :global(.your-class) since rows live inside the grid.

Example
<Grid {rows} {columns} height={480} rowClass={(row) => (row.status === 'Rejected' ? 'rejected' : undefined)} />
getRowId (row: GridRow) => string | number Default (row) => row.id

Identity key for rows: selection, focus, pins and flashes follow it. Override for string/UUID/composite keys.

Example
<Grid {rows} {columns} height={480} getRowId={(row) => `${row.account}:${row.sym}`} />
selectedRowId string | number | null Default null

Controlled "active row" highlight (keyed by getRowId) — for master-detail / list-detail where one row is selected. Independent of the checkbox rowSelection and the cell range selection. null = none.

onRowClick (row: GridRow, event: MouseEvent | KeyboardEvent) => void

Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.

Example
<Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />
hotkeys Hotkeys Default undefined

Keys for the app, run before the grid's own. A binding is a key ('b', 'Delete', 'Space'; a symbol by the character it types: '?', not 'Shift+/'), after any of Shift+, Alt+, Ctrl+, Meta+ and Mod+ (Ctrl or ⌘, either). Case does not matter. The handler is given the focused row and column (null when there is none) and the event; return false to leave the key to the grid. Keys go to the grid, not to a binding, while a cell is edited or an input, button or menu inside the grid has them, and a key a binding took runs it once when held down. A binding that cannot be read, or binds a key twice, throws (BG1001–BG1004).

Example
<Grid {rows} {columns} height={480}
  hotkeys={{ b: ({ row }) => { if (row) buy(row); }, 'Shift+C': cancelAll, Delete: ({ row }) => { if (row) cancel(row); } }} />
sort SortState[] Default undefined

Controlled sort order (multi-key, primary first). When set, the grid reflects this and reports changes via onSortChange instead of holding its own state. Omit for uncontrolled sorting.

Example
<Grid {rows} {columns} height={480} {sort} onSortChange={(next) => (sort = next)} />
onSortChange (sort: SortState[]) => void

Called with the new sort order whenever a header is clicked.

Example
<Grid {rows} {columns} height={480} onSortChange={(sort) => save('sort', sort)} />
columnFilters Record<string, AnyFilter> Default undefined

Controlled column filters (keyed by column key). When set, the grid reflects these and reports changes via onFilterChange instead of holding its own. Omit for uncontrolled filtering.

Example
<Grid {rows} {columns} height={480} filterMenu
  columnFilters={{ sym: { kind: 'text', op: 'starts', q: 'V' }, last: { kind: 'number', op: 'gt', a: 50 } }} />
onFilterChange (filters: Record<string, AnyFilter>) => void

Called with the full column-filter map whenever a header filter changes.

Example
<Grid {rows} {columns} height={480} filterMenu {columnFilters} onFilterChange={(next) => (columnFilters = next)} />
onCellClick ( info: { row: GridRow; column: ColumnDef; value: unknown }, event: MouseEvent, ) => void

Called when a cell is clicked, with its row, column and value. Fires in addition to onRowClick; excluded for the edit input.

Example
<Grid {rows} {columns} height={480} onCellClick={({ row, column }) => column.key === 'sym' && openChart(row.sym)} />
pinnedRows GridRow[] Default []

Rows pinned to the top, always visible above the scroll (a benchmark, a summary, "your position"). Display-only — not virtualized or selectable. Their getRowId must not collide with a row in rows/source — with a derived-flash column (flash: 'auto'), a shared id means the pinned cell and the data-row cell share one flash-tracker entry, so updating one can suppress or trigger a flash on the other. To let users pin rows of rows themselves, use rowPinning.

Example
<Grid {rows} {columns} height={480} pinnedRows={[{ id: -1, sym: 'VN30', last: index }]} />
filterRow boolean Default false

Show a per-column filter input row under the header. Rows must match every non-empty column filter (AND). In-memory mode only.

filterMenu boolean Default false

Enable a per-column header filter menu (lazy-loaded on first open). Each filterable column shows a funnel; the menu's control matches the column type (text/number/date). Override or disable per column with col.filter. Works in source mode too (filters are delegated to the RowSource); set filters need in-memory data.

quickFilter boolean Default false

Show a built-in quick-filter search box above the grid that matches across all column values (ANDed with the filter prop). With a source, its text is sent to getRows as quickFilter.

fillHandle boolean Default false

Show an Excel-style fill handle at the selection's bottom-right corner; drag it to copy the selected value(s) across the extended range (editable columns only). In-memory mode only.

emptyMessage string Default labels.noRows

Message shown when there are no rows.

labels Partial<GridLabels> Default {}

Override any string the grid renders itself (menus, filter editor, pager, aria labels). Merged over English defaults — pass only what changes. Interpolated entries are functions so translations can reorder the parts. Resolved per grid.

Example
<Grid {rows} {columns} height={480} labels={{ noRows: 'Không có dữ liệu', sortAscending: 'Tăng dần' }} />
defaultColumn DefaultColumn Default undefined

Settings applied under every column — its own fields (and a registered cellType) win.

Example
<Grid {rows} {columns} height={480} defaultColumn={{ minWidth: 72, sortable: false }} />
cellTypes Record<string, CellTypeDef> Default undefined

Reusable column types, by name. A column with cellType: 'name' gets that entry's defaults (format, compare, align, filter, width, a component renderer…) under its own fields, and the built-in type named by extends for sorting, filtering and export.

Example
<Grid {rows} {columns} height={480}
  cellTypes={{ qty: { extends: 'number', decimals: 0, width: 80, format: (v) => Number(v).toLocaleString() } }} />
filterTypes FilterTypes Default undefined

Custom filter kinds, by name. A column selects one with filter: 'name'; the header filter menu draws its component and the view keeps rows for which test passes. An unregistered kind filters nothing.

Example
<Grid {rows} {columns} height={480} filterMenu
  filterTypes={{ band: { component: BandFilter, test: (v, f) => Number(v) >= f.min && Number(v) <= f.max } }} />
locale string Default undefined

BCP 47 locale for the grid's own number formatting (aggregation bar, pager row count), and the default for each column's locale. Unset, the runtime's locale.

onReady (api: GridApi) => void

Called once, when the grid mounts, with a handle for imperative actions: scrollToRow, focusCell, patchRows, getSelectedRows, autosizeColumns, exportCSV, getState / applyState and more.

Example
<Grid {rows} {columns} height={480} onReady={(a) => (api = a)} />
onViewportChange (view: ViewportRange) => void

Called with the data rows on screen — the rows in view plus the small buffer rendered around them — whenever that set changes (scroll, sort, filter, new data), never for a tick that only changes values. Pass it createViewportSubscriptions(...).update from bo-grid/realtime to stream just the symbols in view.

Example
<Grid {rows} {columns} height={480} onViewportChange={subs.update} />
flashMotion 'fade' | 'hold' | 'auto' Default 'fade'

How a flash plays. 'fade': the tint fades out. 'hold': the tint holds for the flash window, then drops — no CSS animation, so a busy board restyles and repaints a flashing cell twice instead of every frame. 'auto': fade while few cells flash at once, hold when many do. A flash that lights whole rows (flashNewRows, api.flashCells) counts each cell it lights.

liveSort boolean | number Default false

Keep the sort and filters current while api.patchRows changes values in place. When a patch changes a field the grid sorts or filters by, the rows it changed are merged back into place at most every liveSort ms (true: 500), instead of the whole view being re-sorted. The selection stays on the same rows; nothing moves while a cell is being edited. Off, rows hold their place until api.refresh(). It follows patchRows; values written into rows any other way still need api.refresh().

Example
<Grid {rows} {columns} height={480} liveSort={250} />
pauseOnHover boolean Default true

While the pointer is over the rows, liveSort waits, so a row does not move just as it is clicked; the rows re-sort once the pointer leaves (or the window loses focus, or the tab is hidden). Mouse and pen only. Set false for a screen nobody points at.

staleAfter number Default 0

Dim a row when nothing new has come for it for this many ms: a quote whose feed went quiet. News is a patch (api.patchRows) that changed the row, or a new object for it in rows (the row arrived, or a copy replaced it); a patch that re-sends the values the row already has is not. The row comes back the moment it ticks. Rows already there count from mount, or from when staleAfter is turned on. Dimmed with opacity: var(--bo-stale-opacity, 0.5). 0 turns it off.

flashNewRows boolean | number Default false

Flash a row once when it arrives in rows (a new order, a new fill): every cell, in the neutral flash colour, for this many ms (true: 600). A row arrives when its id is new; a copy of a row already there does not flash, nor do the rows an empty grid gets (a load or a reload), nor a wholesale replacement (more than 64 rows, and more than an eighth of them, at once). Rows on screen only, as with every flash.

Example
<Grid {rows} {columns} height={480} flashNewRows={900} />
columnHover boolean Default false

Highlight the column under the pointer (body and header) — handy on a wide board. One overlay for the whole column, not a class per cell.

rowNumbers boolean Default false

A leading row-number column (1, 2, 3… in view order; data rows only), as in a spreadsheet. Its cells are row headers, so screen readers announce the number with each row.

rowPinning boolean Default false

Let users pin rows above the scroll from the row menu (right-click): "Pin to top" / "Unpin". A pinned row stays in the body too, keeps updating, and is remembered with persistKey and in getState().

findBar boolean Default false

Ctrl/⌘+F in the grid opens a find bar: it matches the cells' displayed text (case-insensitive) and steps through matches with Enter / Shift+Enter, focusing and scrolling to each. Escape closes it. Off by default, as it takes Ctrl+F from the browser while the grid has focus. In-memory rows only.

fitColumns boolean Default false

Scale every column with the grid, in proportion to its width, instead of only flex columns taking up the change: the grid stays responsive without hand-tuned flex weights. A column never gets narrower than its minWidth (64 px, or its own width if smaller, without one) nor wider than its maxWidth; when the floors no longer fit, the grid scrolls sideways. It fits until the user sets a width (a drag, an autosize): from then on the columns keep the widths they were given, until Reset widths in the column menu.

onRowPinChange (ids: (string | number)[]) => void

Called with the pinned row ids (in pin order) whenever they change.

Example
<Grid {rows} {columns} height={480} rowPinning onRowPinChange={(ids) => save('pinned', ids)} />
loading boolean Default false

Show a loading overlay over the grid (for consumer-driven async work in in-memory mode; source mode shows skeleton rows automatically).

rowMenu (row: GridRow) => Array<{ label: string; onSelect: () => void }>

Right-click row menu. Return the items for a row; an empty array shows no menu. Each item runs onSelect and closes the menu.

Example
<Grid {rows} {columns} height={480}
  rowMenu={(row) => [{ label: 'Cancel order', onSelect: () => cancel(row) }, { label: 'Copy id', onSelect: () => copy(String(row.id)) }]} />
detail Snippet<[{ row: GridRow }]>

Master-detail: render an expandable detail panel under a row. Adds a leading expand-toggle column. In-memory mode only (overrides rowHeight).

Example
<Grid {rows} {columns} height={480}>
  {#snippet detail({ row })}<p>{row.sym}: {row.note}</p>{/snippet}
</Grid>
detailHeight number Default 160

Height (px) of the expanded detail panel.

getChildren (row: GridRow) => GridRow[] | undefined

Tree data: return a row's children (undefined/empty = leaf). When set, rows are the roots; the grid renders an indented, expandable tree. In-memory mode; filter/sort/group/paginate are not applied to the tree.

Example
<Grid rows={roots} {columns} height={480} getChildren={(row) => row.children as GridRow[] | undefined} />
loadChildren (row: GridRow) => Promise<GridRow[]>

Async tree data: load a row's children on expand (server-backed trees). Returns a promise; the grid shows a loading row, then caches the result. Pair with hasChildren (a cheap predicate) so chevrons show without loading. Use instead of getChildren. In-memory roots.

Example
<Grid rows={roots} {columns} height={480}
  loadChildren={(row) => fetch(`/api/accounts/${row.id}/children`).then((r) => r.json())}
  hasChildren={(row) => Number(row.childCount) > 0} />
hasChildren (row: GridRow) => boolean

Cheap predicate for whether a row has children (drives the expand chevron without loading). Required with loadChildren; optional with getChildren.

Example
<Grid rows={roots} {columns} height={480} {loadChildren} hasChildren={(row) => row.kind === 'folder'} />
lazyGroups LazyGroup[] Default undefined

Server-side grouping: top-level group summaries (header data, no leaf rows). Each group's rows load on expand via loadGroup. In-memory mode.

Example
<Grid rows={[]} {columns} height={480}
  lazyGroups={[{ key: 'HOSE', count: 412 }, { key: 'HNX', count: 330 }]}
  loadGroup={(key) => fetch(`/api/board/${key}`).then((r) => r.json())} />
loadGroup (key: string) => Promise<GridRow[]>

Load a lazy group's leaf rows on expand (returns a promise; shows a loading row, then caches). Required with lazyGroups.

Example
<Grid rows={[]} {columns} height={480} {lazyGroups} loadGroup={(key) => loadExchange(key)} />
onRowReorder (fromIndex: number, toIndex: number) => void

Enable drag-to-reorder rows via a handle in the first column. Called with the from/to indices on drop — reorder your own rows in here. The indices are positions in the rows as shown, so they match rows while the list is neither sorted nor filtered. Off for grouped, paged, tree and source grids.

Example
<Grid {rows} {columns} height={480}
  onRowReorder={(from, to) => { const next = [...rows]; next.splice(to, 0, ...next.splice(from, 1)); rows = next; }} />
pageSize number Default 0

Rows per page. When > 0 (in-memory mode), shows a pager instead of one long scroll; rows still virtualize within a page. 0 turns paging off.

pageSizeOptions number[] Default undefined

Page-size choices for a dropdown in the pager. Picking one re-pages from page 0. Uncontrolled unless you also drive pageSize and update it from onPageSizeChange.

Example
<Grid {rows} {columns} height={480} pageSize={50} pageSizeOptions={[25, 50, 100]} />
onPageSizeChange (size: number) => void

Called with the new page size when the pager's size dropdown changes.

Example
<Grid {rows} {columns} height={480} {pageSize} pageSizeOptions={[25, 50, 100]} onPageSizeChange={(n) => (pageSize = n)} />
page number Default undefined

Controlled current page (0-based). Omit for uncontrolled paging.

onPageChange (page: number) => void

Called with the new page index when the pager is used.

Example
<Grid {rows} {columns} height={480} pageSize={50} {page} onPageChange={(p) => (page = p)} />
onColumnReorder (keys: string[]) => void

Called with the new column-key order after a header drag-reorder.

Example
<Grid {rows} {columns} height={480} onColumnReorder={(keys) => save('order', keys)} />
onColumnResize (key: string, width: number) => void

Called with a column key + new width after a drag-resize. Only the dragged column is reported; getState() has every width.

Example
<Grid {rows} {columns} height={480} onColumnResize={(key, width) => save(`width:${key}`, width)} />
ariaLabel string Default undefined

Accessible name for the grid (aria-label on the role="grid" root).

filter string Default ''

Quick-search text: a row is kept when any column's value, as text, contains it (case-insensitive). Bind it to your own search input.

groupBy string[] Default []

Column keys to group rows by, outermost first (nested groups with subtotals). In-memory mode only.

Example
<Grid {rows} {columns} height={480} groupBy={['account', 'sym']} />
aggregations AggKind[] Default ['sum', 'avg', 'count', 'min', 'max']

Which stats the selection footer shows over a multi-cell range.

Example
<Grid {rows} {columns} height={480} aggregations={['sum', 'count']} />
persistKey string Default undefined

Remember the user's layout (column order, widths, hidden and pinned columns, pinned rows) in localStorage under this key.

source RowSource Default undefined

Back the grid with a windowed/server data source instead of rows. In source mode, sort + filter are delegated to the source; grouping is not applied.

Example
<Grid rows={[]} {columns} height={480}
  source={{ getRows: ({ range, sorts, filter }) => fetchPage(range.start, range.end, sorts, filter) }} />
onCellEdit (e: CellEditEvent) => void

Called when an editable cell is committed. Update your row data in here.

Example
<Grid {rows} {columns} height={480} onCellEdit={({ row, column, value }) => (row[column.key] = value)} />
cell Snippet<[{ row: GridRow; column: ColumnDef; value: unknown }]>

Render content for type: 'custom' columns.

Example
<Grid {rows} {columns} height={480}>
  {#snippet cell({ row, column })}{#if column.key === 'chart'}<MiniChart sym={row.sym} />{/if}{/snippet}
</Grid>

Column options

Fields of a column definition (ColumnDef), shared by every column type.

Column options from the types
key* string

Field on the row to read for this column's value. Also the column's id: sort, filter, width and hidden state are keyed by it.

header* string

The column's title in the header row.

locale string Default undefined

BCP 47 locale for this column's built-in formatting (price, date, currency, relative). Unset, the grid's locale, then en-US.

value (row: GridRow) => unknown

Computed value: derive this cell's value from the whole row instead of reading row[key] (KPIs, ratios, deltas). Flows through display, sort, filter, aggregation, export and conditional formatting. key still names the column (and is the sort/filter key) but need not be a real row field. Keep it cheap and pure — it's called during sort/filter. Computed columns aren't editable. In-memory mode (a server source owns its own derivations).

Example
{ type: 'percent', key: 'chg', header: 'Chg %', value: (row) => ((Number(row.last) - Number(row.ref)) / Number(row.ref)) * 100 }
spanRows boolean | ((a: GridRow, b: GridRow) => boolean) Default false

Merge this column's cell down over adjacent rows. true joins rows with equal (non-blank) values; a function decides for each adjacent pair. Hierarchical: a run also breaks wherever a spanning column to its left breaks, and at group headers. A drawing rule only — sort, filter, copy and export still see every row's own value. In-memory mode only.

Example
{ type: 'text', key: 'account', header: 'Account', spanRows: true }
colSpan (row: GridRow) => number

How many columns this cell covers for a given row (1 when unset). Covered cells are not drawn. Clamped at a pinned/scrolling boundary and the last column; the leftmost claim wins. Ignored under virtualizeColumns.

Example
{ type: 'text', key: 'sym', header: 'Symbol', colSpan: (row) => (row.kind === 'note' ? 4 : 1) }
width number Default 96

Width in px. A flex column starts from it where columns have fixed widths (pinned columns, virtualizeColumns), and fitColumns scales columns in proportion to it.

minWidth number Default 48

Smallest width (px) a drag-resize or autosize can give this column; never less than 48. Under fitColumns, also the narrowest the column gets before the grid scrolls sideways (64 px without one, or the column's width if smaller).

maxWidth number Default Infinity

Largest width (px) a drag-resize, an autosize, autoWidth or fitColumns can give this column.

flex number Default undefined

flex-grow weight: the column stretches to fill the space the fixed-width columns leave, shared by weight. When the columns no longer fit, the grid scrolls sideways, counting a flex column as its width, else its minWidth, else 64 px. Unset, the column keeps its width.

align Align Default 'right' for number types, 'left' for the rest

Which side the header, cells and footer sit on. Set it to line a custom column's values up under its title.

Example
{ type: 'custom', key: 'pnl', header: 'P&L', align: 'right' }
cellClass string | ((value: unknown, row: GridRow) => string | undefined) Default undefined

Extra class(es) for this column's data cells — a static string, or a function of the cell value/row for conditional styling. Target via :global(.bo-grid .c.your-class).

Example
{ type: 'price', key: 'last', header: 'Last', cellClass: (v, row) => (Number(v) >= Number(row.ceiling) ? 'at-ceiling' : undefined) }
headerClass string Default undefined

Extra class(es) for this column's header. Target via :global.

headerTooltip string Default undefined

Show a styled tooltip on the column header (the same themed bubble as cell tooltip) — describe what the column means, units, caveats. Hovering anywhere on the header reveals it.

headerInfo boolean Default false

Render a small ⓘ info icon in the header as a visible cue that a headerTooltip is available. Ignored without headerTooltip.

flash boolean | 'auto' | 'up-down' | 'change' Default false

Flash the cell when its value changes. - 'auto' (alias 'up-down') — the grid derives it: green on a rise, red on a fall, per cell. Nothing to maintain in app code; just stream new values in. This is what you want for a live price board. - 'change' — a neutral flash on any change, numeric or not (status, qty). - true — legacy row-driven mode: the flash follows the row's own flashSeq/flashDir, so every flash: true column in a row flashes together, in one direction, and you bump the counter yourself. Derived modes never flash on first paint or when a row scrolls into view.

flashMs number Default 300

Flash duration in ms — derived modes only ('auto'/'up-down'/ 'change'); has no effect on legacy flash: true, which flashes for the CSS animation's fixed duration regardless. Raise it for a slow feed, lower it for a fast one so flashes don't smear together.

flashColor boolean Default true

Tint the text in the tick's direction while flashing. Set false when the column colours its own text — e.g. price-limit tones on a VN board (ceiling / floor / reference) — so the flash only lights the background.

showChange boolean | ShowChangeOptions Default false

Show the size of each change beside the value for a moment — ▲0.15 in the up colour, ▼ down — held, then faded. The delta is the last change, written in the column's own format. Numeric values only; first paint and rows scrolling into view show nothing. { format, ms } sets the text (format(delta, row) gets the signed change) and how long it shows (1000 ms unless set). In a narrow column the delta gives way first: the value itself is never cut for it. Meant for one or two key columns (the match price): each change runs a short animation, so a busy board with it on every column pays real frame time.

Example
{ type: 'price', key: 'last', header: 'Last', flash: 'auto', showChange: { ms: 1500, format: (d) => `${d > 0 ? '+' : ''}${d.toFixed(2)}` } }
updateMs number Default undefined

Repaint this column at most every this many ms under api.patchRows. Each tick is still written into the row at once (sort, filters, totals and other columns see it), but the cell shows the latest value on its next beat: a value that changes on every tick and is read at a glance (total volume, foreign buy and sell) stops repainting every frame. The first change after a quiet spell shows at once. A flash or showChange plays on the repaint, against the value shown before. For the field the column shows (key), not a value column; when several columns show a field, it waits only if they all set this. Unset, the cell repaints on every tick.

sortable boolean Default true

Set false to disable header-click sorting on this column.

compare (a: unknown, b: unknown) => number

Custom ascending comparator for this column's values (e.g. enum priority or natural sort). Direction is applied by the grid. In-memory mode only.

Example
{ type: 'badge', key: 'status', header: 'Status', compare: (a, b) => RANK[String(a)] - RANK[String(b)] }
groupAgg AggKind Default undefined

When grouping is active (or with footer), show this aggregate of the column on group headers and the totals row.

Example
{ type: 'volume', key: 'qty', header: 'Qty', groupAgg: 'sum' }
pinned boolean | 'left' | 'right' Default false

Pin this column so it stays visible during horizontal scroll. true / 'left' pins to the left edge, 'right' to the right.

editable boolean Default false

Allow inline editing (double-click, Enter or F2 on the focused cell, or type to replace). Number types edit as numbers, date as a date. Display types (badge, boolean, rating, progress, tags, link, avatar, relative, sparkline, custom) edit only with an editor; a computed (value) column never does.

validate (value: unknown, row: GridRow) => boolean

Validate an edited value before it commits (inline edit, custom editor, paste, fill). Return false to reject and keep the old value. Receives the parsed value.

Example
{ type: 'number', key: 'qty', header: 'Qty', editable: true, decimals: 0, validate: (v) => Number(v) > 0 && Number(v) % 100 === 0 }
parse (raw: string, row: GridRow) => unknown

Turn typed or pasted text into the stored value (runs before the built-in number/date coercion, which it replaces). Return undefined or a non-finite number to reject the input.

Example
{ type: 'number', key: 'qty', header: 'Qty', editable: true, parse: (raw) => Number(raw.replace(/[,\s]/g, '')) }
editor Component<CellEditorProps>

Svelte component that edits this cell in place of the built-in input. Makes any column type editable (with editable: true), including display widgets like rating or tags. It calls commit(value) to save — any value, no parsing — or cancel(); Escape cancels.

Example
{ type: 'rating', key: 'score', header: 'Score', editable: true, editor: StarPicker }
options string[] Default undefined

Editable choices: when set, editing renders a <select> of these options instead of a text input (enum/status columns).

Example
{ type: 'text', key: 'side', header: 'Side', editable: true, options: ['Buy', 'Sell'] }
tooltip boolean | ((value: unknown, row: GridRow) => string) Default false

Show a styled floating tooltip on each cell (themed, instant — replaces the native title). true shows the full formatted value (handy when content truncates); a function returns custom text per value/row (any column type, including custom). Return '' to suppress for a given cell.

Example
{ type: 'text', key: 'sym', header: 'Symbol', tooltip: (_, row) => String(row.name ?? '') }
wrap boolean Default false

Let long content wrap to multiple lines instead of truncating with an ellipsis. Pair with a taller rowHeight.

format (value: unknown, row?: GridRow) => string

Custom display formatter, overriding the built-in type formatter. Applies to display, tooltip, copy and (formatted) export. row is absent for aggregate cells.

Example
{ type: 'number', key: 'qty', header: 'Qty', format: (v) => Number(v).toLocaleString('vi-VN') }
render (ctx: CellRenderContext) => string | Node | null | undefined

JS cell renderer — the framework-agnostic alternative to the cell snippet, for React/Vue/vanilla (and the <bo-grid> web component). Return an HTMLElement/Node (safe — appended as-is) or an HTML string (inserted as innerHTML — YOU must sanitize any untrusted data; prefer returning a Node for that). Overrides the cell's display only — sort, filter, tooltip, copy and export still use the value/format.

Example
{ type: 'text', key: 'sym', header: 'Symbol', render: ({ value }) => Object.assign(document.createElement('b'), { textContent: String(value) }) }
component Component<CellTypeProps>

Svelte component that draws this cell (receives value, row, column and the formatted text). Display only, like render. Usually supplied by a registered cellTypes entry, but any column can set it. It updates in place as its props change; under api.patchRows, row is a fresh view of the patched row on each patch (same fields, new identity), so read it reactively ($derived, markup) rather than once at mount.

Example
{ type: 'text', key: 'country', header: 'Country', component: CountryCell }
resizable boolean Default true

Set false to disable drag-to-resize on this column.

autoWidth boolean Default false

Widen the column when a value on screen no longer fits (a price gaining a digit, a running total), checked at most every 500 ms. It only grows, so values flickering wider and narrower don't make it jump; maxWidth caps it, and a width the user dragged or autosized takes over.

group string Default undefined

Parent header label. Consecutive columns sharing a group render under a spanning header, which keeps the width of its columns (flex ones too).

filter false | FilterKind | (string & {}) Default 'date' for date columns, 'number' for number types, else 'text'

Header filter-menu control for this column (requires filterMenu on <Grid>). 'set' shows a value checklist, a registered filterTypes name draws that filter, and false disables filtering for this column.

Example
{ type: 'badge', key: 'status', header: 'Status', filter: 'set' }
dataBar DataBarConfig Default undefined

Conditional formatting — a horizontal bar painted behind the cell value, scaled across the column's value range. The range is auto-computed over the current view (in-memory) unless min/max are given; pass min: 0 for absolute proportional bars. Bars cross a zero baseline when the range spans negatives. Numeric columns only.

Example
{ type: 'volume', key: 'vol', header: 'Volume', dataBar: { min: 0 } }
icons IconRule[] Default undefined

Conditional formatting — show an icon beside the value, chosen by the highest threshold at that is ≤ the cell value (e.g. ▲ ● ▼ by sign or band). Numeric columns only.

Example
{ type: 'percent', key: 'chg', header: 'Chg %', icons: [{ at: -Infinity, icon: '▼', tone: 'down' }, { at: 0, icon: '●' }, { at: 0.01, icon: '▲', tone: 'up' }] }
colorScale ColorScaleConfig Default undefined

Conditional formatting — tint the cell background across the column's value range (a soft heat ramp). Auto-ranged over the current view unless min/ max are given; pass mid for a 3-stop diverging scale (e.g. mid: 0). Numeric columns only.

Example
{ type: 'percent', key: 'chg', header: 'Chg %', colorScale: { mid: 0 } }

Column types

A column's type, and the options each type adds.

typeOptions it adds
textsub?: string
pricenone
percentnone
volumenone
numberdecimals?: number
datedateStyle?: DateStyle
currencycurrency?: string, decimals?: number
relativenone
heatmapmin: number, max: number, decimals?: number
sparklinesparkKey: string
progressmin?: number, max?: number
ratingmax?: number
tagsnone
badgetones?: Record<string, BadgeTone>
booleantrueLabel?: string, falseLabel?: string
avatarsub?: string
linkhref?: (row: GridRow) => string, newTab?: boolean
customnone
(cellType)cellType: string

Grid API

Methods of the grid handle: from onReady in Svelte, the ref in React and Vue, or onReady in the element's config.

Grid API from the types
scrollToRow (key: string | number, align?: ScrollAlign): boolean

Scroll a row into view by its id; align is 'nearest' unless given. Returns false when the row is not in the current view (filtered out, not loaded, or unknown).

Example
api.scrollToRow(order.id, 'center');
focusCell (rowKey: string | number, columnKey: string): boolean

Focus (and select) one cell by row id and column key. Returns false when the row or column is not currently visible.

Example
if (!api.focusCell(row.id, 'qty')) api.scrollToRow(row.id);
getFocusedCell (): { row: GridRow; column: ColumnDef } | null

The focused cell's row and column, or null when no data cell has focus (nothing focused yet, or the focus is on a group row). For hotkeys that act on the row the user is on.

Example
const cell = api.getFocusedCell();
if (cell) openTicket(cell.row);
getSelectedRows (): GridRow[]

Ticked rows (needs rowSelection), in view order.

Example
const ids = api.getSelectedRows().map((row) => row.id);
autosizeColumns (keys?: string[]): void

Fit columns to their content, as the column menu's Autosize does. All resizable columns when keys is omitted. The other columns keep their widths (flex ones are held where they are).

Example
api.autosizeColumns(['sym', 'name']);
exportCSV (filename?: string): Promise<void>

Download the current view (after filter and sort) as CSV, named export.csv unless filename is given.

Example
await api.exportCSV(`orders-${new Date().toISOString().slice(0, 10)}.csv`);
getState (): GridState

Snapshot of the user's layout: order, widths, hidden, pins, sorts, filters. A plain object, safe to JSON.stringify and store.

Example
localStorage.setItem('orders-layout', JSON.stringify(api.getState()));
applyState (state: unknown): boolean

Restore a snapshot, reconciled against the columns as they are now. Returns false when the state is unusable (foreign version, not an object).

Example
const saved = localStorage.getItem('orders-layout');
if (saved) api.applyState(JSON.parse(saved));
patchRows (patches: Iterable<readonly [string | number, Record<string, unknown>]>): number

Realtime fast path: write [rowId, fields] patches into the rows in place and repaint only the rendered rows they change. Rows may be plain objects (no $state proxy) — far cheaper per write in a busy feed. Feed it a tick stream: createTickStream({ apply: (b) => api.patchRows(b) }). Sort and filter catch up on the next view change, as for any live data (or at once, with liveSort). A patch's id is the row's getRowId value (row.id unless set). Returns how many rows changed.

Example
api.patchRows([
  [1, { last: 68.6, vol: 1_204_300 }],
  [2, { last: 121.1 }],
]);
refresh (): void

Re-run filter and sort against the rows' current values. Values that change in place (a feed) never re-sort the view by themselves — that would mean re-sorting on every tick — so call this when the order should catch up, e.g. every second on a "top movers" board (or use liveSort).

Example
const timer = setInterval(() => api.refresh(), 1000);
flashCells (opts?: FlashCellsOptions): number

Flash cells on demand — a fill, an alert, a row to notice — with the tick animation, whether or not their values changed. Only rows on screen flash. Value cells flash (numbers, text); cells that draw their own content (badges, sparklines, components) do not. Returns how many rows it flashed.

Example
api.flashCells({ rows: [fill.orderId], dir: fill.side === 'buy' ? 'up' : 'down', ms: 800 });
pinRow (key: string | number, pinned?: boolean): boolean

Pin a row above the scroll (or unpin it with pinned: false), as the row menu does under rowPinning; pinned is true unless given. The row stays in the body too. Returns false when no row has that id.

Example
api.pinRow(row.id);
getPinnedRowIds (): (string | number)[]

Ids of the rows pinned at runtime, in pin order.

Example
for (const id of api.getPinnedRowIds()) api.pinRow(id, false);
openFind (query?: string): void

Open the find bar (findBar), optionally searching for query at once.

Example
api.openFind('VNM');