API reference

A tiny, fast Svelte 5 data grid for fintech UIs — virtual scroll, canvas sparklines, batched realtime updates, sort/filter, selection, grouping, a server-side data source, export, and column pinning.

Install

npm i bo-grid          # peer: svelte@^5
npm i xlsx             # optional, only for exportXLSX()
import { Grid, type ColumnDef, type GridRow } from 'bo-grid';

<Grid>

The grid component. Renders rows from an in-memory array or a RowSource. Not on Svelte? A framework-agnostic <bo-grid> custom element ships at bo-grid/element for React / Vue / Angular / vanilla — see docs/frameworks.md.

PropTypeDescription
rows*GridRow[]Row data (in-memory mode). Pass [] when using source.
columns*ColumnDef[]Column configuration. See Columns.
height*numberViewport height in px.
filterstringQuick-filter; matches across column values. Default ''.
filterRowbooleanPer-column filter input row under the header (AND across columns). In-memory mode only.
filterMenubooleanHeader filter menu (funnel per column) with type-aware controls (text/number/date) + set filter (col.filter: 'set'). Lazy-loaded on first open. In-memory mode only.
quickFilterbooleanBuilt-in search box above the grid; matches across all columns (ANDed with filter). In-memory mode only.
fillHandlebooleanExcel-style fill handle at the selection's corner; drag to copy the selected value(s) across (editable columns). In-memory mode only.
columnMenubooleanPer-column header menu (⋮ or Alt+): sort, pin, autosize, hide (all runtime).
columnsPanelbooleanA "Columns" button opening a checklist panel to toggle column visibility (restore hidden columns). Lazy-loaded.
virtualizeColumnsbooleanRender only columns in the horizontal window (+ overscan) for 100+ column grids; forces fixed-width scroll, pinned columns always render.
onColumnVisibilityChange(hidden: string[]) => voidCalled with all hidden column keys when the runtime set changes (column-menu hide/show).
emptyMessagestringText shown when there are no rows. Default 'No matching rows'.
loadingbooleanShow a loading overlay (consumer-driven async in in-memory mode).
rowMenu(row) => {'{'} label, onSelect {'}'}[]Right-click row menu items; each runs onSelect and closes. Keyboard: ContextMenu / Shift+F10 opens it at the focused cell.
detailSnippet<[{'{'} row {'}'}]>Master-detail panel under each row; adds a leading expand toggle. In-memory mode.
detailHeightnumberHeight (px) of the expanded detail panel. Default 160.
getChildren(row) => GridRow[]Tree data: return a row's children. rows become roots; renders an indented, expandable tree.
loadChildren(row) => Promise<GridRow[]>Async tree: load children on expand (server-backed); shows a loading row, then caches. Pair with hasChildren.
hasChildren(row) => booleanCheap predicate for the expand chevron (no load). Required with loadChildren.
lazyGroups{ key; label?; count?; agg? }[]Server-side grouping: group summaries; headers show the count + preformatted agg. Rows load via loadGroup.
loadGroup(key) => Promise<GridRow[]>Load a lazy group's rows on expand (loading row, then cached). Required with lazyGroups.
onRowReorder(from, to) => voidDrag-to-reorder rows via a first-column handle; reorder your own data. Flat unsorted lists.
pageSizenumberRows per page (> 0 shows a pager instead of one long scroll). In-memory mode. Default 0.
page / onPageChangenumber / (page) => voidControlled current page (0-based) + change callback (URL-sync paging).
ariaLabelstringAccessible name for the grid (aria-label on the role="grid" root).
onColumnReorder(keys: string[]) => voidNew column-key order after a header drag-reorder.
onColumnResize(key, width) => voidColumn key + new width after a drag-resize (persist layout server-side).
groupBystring[]Column keys to group by (nested). In-memory mode only. Default [].
aggregationsAggKind[]Stats shown in the selection footer. Default all five.
persistKeystringPersist the user's column order and widths to localStorage under this key.
sourceRowSourceWindowed/server data source. See Server-side.
resizablebooleanAllow drag-to-resize column widths. Default true; opt out per column with resizable: false.
rowSelectionbooleanShow a leading checkbox column for whole-row selection (keyed by getRowId); Space toggles the focused row. Default false.
getRowId(row) => string | numberIdentity key for selection. Defaults to row.id; override for string/composite keys.
onRowSelectionChange(ids: number[]) => voidCalled with the selected row ids whenever the selection changes.
hiddenColumnsstring[]Column keys to hide (controlled). Drive your own column-picker UI. Default [].
rowClass(row: GridRow) => stringExtra CSS class(es) per data row. Target with :global(.bo-grid .row.your-class).
onRowClick(row, event) => voidFired when a data row is activated by click or Enter.
onCellClick({ row, column, value }, event) => voidFired when a cell is clicked (in addition to onRowClick).
sortSortState[]Controlled sort order (multi-key). Omit for uncontrolled sorting.
onSortChange(sort: SortState[]) => voidCalled with the new sort order on each header click.
columnFiltersRecord<string, ColumnFilter>Controlled column filters (keyed by column key). Omit for uncontrolled filtering.
onFilterChange(filters) => voidCalled with the full column-filter map whenever a header filter changes.
footerbooleanPinned totals row: each column with a groupAgg aggregates all rows. In-memory mode only.
pinnedRowsGridRow[]Rows pinned to the top, always visible above the scroll (display-only).
rowHeightnumber | (row, i) => numberUniform row height in px, or a function for variable heights (in-memory only). Default 36.
theme'dark' | 'light' | GridThemeBuilt-in preset or a custom token map. Default 'dark'.
onCellEdit(e: CellEditEvent) => voidCalled when an editable cell commits (inline edit or paste).
<Grid {rows} {columns} filter={q} groupBy={['sector']} height={640} />

GridRow interface

The minimal contract every row must satisfy; add your own data fields alongside.

interface GridRow {
  id: number;            // stable identity (used as the render key)
  flashSeq: number;      // increment to trigger a cell flash
  flashDir: 'up' | 'down';
  [field: string]: unknown;
}
Make hot fields (price, volume…) reactive $state so updates flash and re-render only the changed cell — not the whole table. Bump flashSeq on each tick to flash a flash: true column.

Columns

ColumnDef is a discriminated union on type. All variants share these base options:

OptionTypeDescription
key*stringRow field to read.
header*stringHeader label.
widthnumberFixed width (px).
minWidth / maxWidthnumberClamp the column width while drag-resizing.
flexnumberflex-grow weight; column fills remaining space.
align'left' | 'right'Header/text alignment.
cellClassstring | (value, row) => stringExtra class(es) on the column's cells (static or conditional). Target via :global.
headerClassstringExtra class(es) on the column header.
flashbooleanAmber flash on value change (drives off flashSeq).
sortablebooleanSet false to disable header-click sorting.
compare(a, b) => numberCustom ascending comparator (enum priority, natural sort). In-memory mode.
groupstringParent header label; consecutive columns sharing it render under one spanning header.
filter'text' | 'number' | 'date' | 'set' | falseHeader filter-menu control for this column (with filterMenu). Defaults to the column type; false disables it.
groupAggAggKindShow this aggregate on group headers.
pinnedboolean | 'left' | 'right'Pin to the left (true/'left') or right edge; stays visible during horizontal scroll.
resizablebooleanSet false to disable drag-to-resize for this column.
editablebooleanAllow inline editing (double-click / Enter, or type-to-edit). Numeric columns use a number input, date columns a date picker.
validate(value, row) => booleanReject an edit (return false) to keep the old value.
optionsstring[]Edit via a <select> of these choices (enum columns).
tooltipbooleanSet a native title tooltip (full value) on each cell.
format(value, row?) => stringCustom display formatter (overrides the type formatter; applies to copy/export too).
value(row) => unknownComputed column: derive the cell's value from the whole row (KPIs, ratios). Flows through sort/filter/aggregation/export. Not editable; in-memory.
dataBar{ min?; max?; color?; negative? }Conditional formatting: in-cell bar behind the value, auto-ranged over the view (or explicit min/max); diverges around zero for signed columns.
icons{ at: number; icon: string; tone? }[]Conditional formatting: icon beside the value, chosen by the highest threshold at ≤ the value; tone sets its colour.
colorScale{ min?; mid?; max?; colors? }Conditional formatting: tint the cell background across the value range (auto-ranged over the view); mid gives a 3-stop diverging scale.

Column types

typeExtra optionsRenders
'text'sub?: stringBold value + optional dim sub-line (second field).
'price'2-decimal number, monospace.
'percent'Signed %, green/red.
'volume'K/M/B suffix.
'number'decimals?: numberFixed-decimal number.
'date'dateStyle?: 'short'|'medium'Formatted date (value is epoch ms).
'currency'currency?; locale?; decimals?Localized currency via Intl.NumberFormat (default USD/en-US).
'relative'Epoch ms rendered as relative time (e.g. 3 hours ago).
'heatmap'min: number; max: numberDiverging red→green background ramp.
'sparkline'sparkKey: stringCanvas candlesticks from a Candle[] field.
'progress'min?: number; max?: numberIn-cell progress bar (value mapped to min..max).
'rating'max?: numberStar rating (value of max, default 5).
'tags'value: string[]Chips from an array (or comma-separated string).
'badge'tones?: Record<string, BadgeTone>Status pill; colour per value via tones (up/down/amber/info/neutral).
'boolean'trueLabel?; falseLabel?: string✓ / ✕ with optional labels.
'avatar'sub?: stringInitials circle + name (optional secondary sub field).
'link'href?: (row) => string; newTab?Anchor; href defaults to the value. URLs sanitized (javascript:/data: blocked).
const columns: ColumnDef[] = [
  { type: 'text',      key: 'symbol', sub: 'sector', header: 'Symbol', pinned: true },
  { type: 'price',     key: 'price',  header: 'Price', flash: true, groupAgg: 'avg' },
  { type: 'percent',   key: 'changePct', header: 'Chg %' },
  { type: 'heatmap',   key: 'changePct', header: 'Heat', min: -5, max: 5 },
  { type: 'volume',    key: 'volume', header: 'Volume', groupAgg: 'sum' },
  { type: 'sparkline', key: 'candles', sparkKey: 'candles', header: 'Trend', flex: 1 },
];

Click a header to sort (asc → desc → off); Shift+click more headers to sort by several columns at once. Drag a header to reorder, or its right-edge grip to resize (double-click the grip to reset).

Conditional formatting

Data bars, icon sets and colour scales paint analytics cues into numeric cells. Data bars auto-range over the current view (or set min/max; min: 0 for absolute bars) and diverge around a zero baseline for signed columns. Icon sets pick the rule whose at threshold is the greatest one ≤ the value. Colour scales tint the cell background across the range (a soft heat ramp; mid makes it diverging). All compose with flashing cells and add nothing to the core when unused.

const columns: ColumnDef[] = [
  { type: 'volume', key: 'marketValue', header: 'Mkt Value', dataBar: { min: 0 } },
  { type: 'number', key: 'pnl', header: 'P&L', decimals: 0,
    dataBar: {},                                  // auto-ranged, diverges around 0
    icons: [
      { at: -Infinity, icon: '▼', tone: 'down' },
      { at: 0,         icon: '▲', tone: 'up' },
    ] },
  { type: 'number', key: 'pnlPct', header: 'P&L %', decimals: 1,
    colorScale: { mid: 0 } },                      // diverging tint around 0
];

Computed columns

Set value: (row) => … to derive a column from the whole row (KPIs, ratios, deltas). The derived value flows through display, sort, filter, group/footer aggregation, conditional formatting, export and copy — like a real column. key still names the column but need not be a real field; computed columns aren't editable. In-memory mode; keep value() cheap and pure (it runs during sort/filter).

{ type: 'price', key: 'total', header: 'Total',
  value: (row) => row.qty * row.price, groupAgg: 'sum' }

Charts companion

For dashboards, bo-grid/charts ships tiny, dependency-free SVG charts. It is a separate import, so it adds nothing to the grid core (~2 KB gzip on its own). Use them standalone or inside a grid cell via a custom column. Theme with color/colors props or --boc-color / --boc-1…6 CSS vars.

ComponentKey propsRenders
LineChartdata: number[]; area?; color?Line (optional filled area).
BarChartdata: number[]; gap?; color?Bars from a zero axis (signed-aware).
DonutChartdata: number[] | {value,color?}[]; thickness?Donut, or pie when thickness ≥ size/2.
StackedBarChartdata: number[][]; grouped?; seriesLabels?Multi-series bars (data[series][category]) — stacked, or grouped side-by-side.
Legenditems: {label, color?}[]Swatch + label list, palette-aware.

Bar / stacked / donut elements carry an SVG <title> for native hover tooltips (the value, plus a label when set).

import { LineChart, BarChart, DonutChart } from 'bo-grid/charts';

<LineChart data={[3, 5, 4, 8, 6, 9]} area />
<DonutChart data={[{ value: 5 }, { value: 3 }]} />

The SVG geometry helpers (linePoints, barRects, donutArcs, extent, …) are exported too, for building your own charts.

Selection & keyboard

Click a cell, then drag or Shift+click to extend a rectangular selection. When more than one cell is selected a footer shows live Sum / Avg / Count / Min / Max over the numeric cells in the range.

KeysAction
Move the focus cell.
Shift + arrowsExtend the selection.
Home / EndFirst / last column in the row (add Ctrl/⌘ for the first / last cell).
PageUp / PageDownMove up / down by one viewport page.
Ctrl/⌘ + ASelect all.
Ctrl/⌘ + CCopy selection as TSV (Excel-pasteable).
Ctrl/⌘ + VPaste TSV into editable cells from the top-left of the selection.
Ctrl/⌘ + Z / YUndo / redo edits, paste and fill (a paste or fill is one step).
Alt + Open the column menu at the focused column (with columnMenu).
Enter / type a characterEdit the focused cell (type-to-edit seeds the editor with the key), or activate onRowClick.
SpaceToggle row selection for the focused row (with rowSelection).
ContextMenu / Shift+F10Open the row context menu (with rowMenu) at the focused cell.
EscClear the selection.

Grouping

Pass groupBy (column keys) for single or nested grouping. Groups are collapsible, headers stick to the top while scrolling, and any column with a groupAgg shows a live subtotal. Group headers are the same height as rows, so virtual scrolling stays smooth.

<Grid {rows} {columns} groupBy={['sector', 'exchange']} height={640} />

Server-side data

Back the grid with a RowSource to load only the visible window — the dataset can be far larger than memory. Sort and filter are delegated to the source; unloaded rows render as skeletons. (Grouping is client-only.)

RowSource interface

interface RowSource {
  getRows(p: {
    range: { start: number; end: number };  // half-open window
    sort: SortState | null;       // primary key (= sorts[0])
    sorts?: SortState[];           // full multi-column order
    filter: string;
  }): RowSourceResult | Promise<RowSourceResult>;
}
interface RowSourceResult { rows: GridRow[]; total: number; }
const source: RowSource = {
  async getRows({ range, sort, filter }) {
    const r = await fetch(`/api/rows?offset=${range.start}&limit=${range.end - range.start}`);
    return r.json(); // { rows, total }
  },
};
<Grid {columns} {source} height={640} />

createArraySource() function

Adapts an in-memory array to RowSource — handy for client-side data or testing the path.

createArraySource(rows, { latency?: number, filterKeys?: string[] }): RowSource

Export & import

FunctionSignature
toCSV(rows, columns, opts?) => string
exportCSV(filename, rows, columns, opts?) => void
exportXLSX(filename, rows, columns, opts?) => Promise<void>
parseCSV(text, columns?) => GridRow[]
parseTSV(text, columns?) => GridRow[]
parseJSON(text) => GridRow[]
rowsFromObjects(objects) => GridRow[]
parseRows(text, columns?) => GridRow[]
parseCSVMatrix(text) => string[][]
toHTMLTable(rows, columns) => string
printTable(rows, columns, { title? }) => void

toHTMLTable/printTable render all rows (the live grid virtualizes) to a clean, escaped table — for printing or PDF capture.

parseCSV/parseTSV are the inverse of toCSV (RFC4180-aware): map headers to columns, coerce numeric/date columns, stamp id + flash fields. rowsFromObjects adapts a JSON/API array (own id or index + flash fields). parseRows auto-detects JSON/TSV/CSV — handy for a paste handler.

ExportOptions: { formatted?: boolean; header?: boolean }. Numeric columns export as raw numbers unless formatted is set; sparkline columns are skipped. exportXLSX dynamic-imports the optional xlsx peer (separate lazy chunk).

Package exports

Components

GridSparkline

Formatters

fmtPricefmtPercent fmtVolumefmtDate

Helpers

aggregateheatColor drawCandlessetupHiDpiCanvas

Export

toCSVexportCSVexportXLSX

Data source

createArraySource

Types

ColumnDefGridRowSortState AggKindCandleRowSource

Theming

Dark-first and self-contained — no CSS import required. Override any token by setting the matching --bo-grid-* custom property on an ancestor, or pass the theme prop a custom token map.

Six built-in presets are exported: darkTheme, lightTheme, highContrastDark, highContrastLight, midnightTheme, terminalTheme — plus a themePresets name→preset map and a ThemePreset type for a theme picker. Pass any to theme.

TokenDefaultTokenDefault
--bo-grid-bg#1a1a1a--bo-grid-up#34d399
--bo-grid-header-bg#0f0f0f--bo-grid-down#f87171
--bo-grid-row-a#131313--bo-grid-amber#f59e0b
--bo-grid-row-b#0f0f0f--bo-grid-sel-fillindigo 16%
--bo-grid-row-hover#1f1f24--bo-grid-sel-border#6366f1
--bo-grid-text#e5e5e5--bo-grid-header-h28px
--bo-grid-text-dim#8a8a8a--bo-grid-monoSF Mono…
--bo-grid-borderwhite 6%--bo-grid-sansInter…
--bo-grid-schemedarknative controls (checkbox / date / spinners / scrollbars) — light or dark
--bo-grid-radius8px--bo-grid-font-size13px
--bo-grid-cell-pad8pxhorizontal cell/header padding (density lever; pair with rowHeight)
.light-grid {
  --bo-grid-bg: #fff;
  --bo-grid-text: #1a1a1a;
  --bo-grid-up: #16a34a;
  --bo-grid-down: #dc2626;
}