// drop in via CDN — use the explicit .umd.js path so the CDN
// serves it as application/javascript (not application/node).
<script src="https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.umd.js"></script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.css">
A grid worth shipping.
Sixty seconds, three files, zero dependencies.
Boostgrid is a modern data grid for Bootstrap 5. Written in TypeScript, no jQuery, polished for the modern browser. It renders, sorts, paginates and selects ten thousand rows in a hundred milliseconds, and the entire bundle weighs less than a single SVG icon.
- 16.7kb brotli · minified · standalone
- 10k/ 105ms first paint, ten thousand rows
- 0deps no jQuery, no runtime imports
- 17methods stable, typed public API
Install
three ways in$ npm install boostgrid
import { attach } from "boostgrid";
import "boostgrid/style.css";
attach("#grid");
// .NET / Visual Studio
PM> Install-Package Boostgrid
// drops boostgrid.min.{js,css}
// into ~/Scripts and ~/Content
Host-page requirements
what the page must load
Boostgrid is a UI component, not a full bundle. The host page must
also load Bootstrap 5's CSS and JavaScript bundle (the
toolbar's page-size picker and column-visibility menu are
Bootstrap dropdowns; without bootstrap.Dropdown the
buttons render but never open), plus an icon font matching the
configured icons option (Bootstrap Icons by default,
or Font Awesome).
<!-- 1. Bootstrap 5 CSS -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<!-- 2. Bootstrap 5 JS bundle (required for the page-size + columns dropdowns) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<!-- 3. An icon font matching the configured `icons` option -->
<!-- Default: Bootstrap Icons. Swap for Font Awesome when using `icons: fontAwesomeIcons`. -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- 4. Boostgrid itself -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.css">
<script src="https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.umd.js" defer></script>
Minimal markup
html + data-attributes
Mark up a regular Bootstrap 5 table, then either set
data-toggle="boostgrid" for auto-init or call attach().
<table id="grid" class="table table-hover" data-toggle="boostgrid"
data-selection="true" data-multi-select="true">
<thead>
<tr>
<th data-column-id="id" data-identifier="true" data-type="numeric">ID</th>
<th data-column-id="sender" data-order="asc">Sender</th>
<th data-column-id="received">Received</th>
</tr>
</thead>
<tbody>…</tbody>
</table>
Examples
live, copy-pastablePick an example on the left to see it run with the real bundle.
The full specification.
Options
passed tonew Boostgrid(t, …)
| Option | Type | Default | Description |
|---|
Methods
on a Boostgrid instance| Method | Returns | Description |
|---|
Events
DOM events + typedgrid.on()
| Event | Payload | Description |
|---|
Ajax wire format
whenajax: true
When ajax: true is set, the grid POSTs an
AjaxRequest body to url and
expects an AjaxResponse back. Both shapes
are reshapable end-to-end via the
requestHandler and
responseHandler options if your existing
backend uses a different layout (DataTables-style,
JSON:API, etc.).
REQUESTAjaxRequest
| Field | Type | Description |
|---|
RESPONSEAjaxResponse
| Field | Type | Description |
|---|
Performance
why it is fast-
01
Single delegated listener
One click handler per grid; row buttons route through
data-bg-action—no per-row bindings to rebuild on render. -
02
Memoized derived view
Filter, sort and pagination are independent layers. Changing pages doesn't re-sort; sorting doesn't re-filter.
-
03
O(1) selection lookups
A
Map<id, Row>is built once on data load—select(),remove()andgetare constant-time. -
04
Debounced search
The search input coalesces keystrokes (200 ms by default) so the filter pipeline doesn't run on every letter.
-
05
CSS containment
The grid root carries
contain: content; rows usecontain: layout style—paint and layout stay inside the grid subtree. -
06
Bundle guarded at
< 18 KBEvery PR runs
size-limit. Current ship size is 16.7 KB brotli — numbers tracked against the actual build, not estimated.
Every line that shipped.
A running log of every release. Versions follow semver: major bumps are breaking, minor bumps add features, patches are bug fixes only.
One-line bug fix with an outsized symptom. The
column-visibility checkbox showed and hid columns
correctly but never visually toggled — it appeared
stuck checked. The single delegated click handler was
calling preventDefault() on every
action element, including the checkbox, which reverted
the browser's native toggle. The handler now leaves
checkbox / radio controls alone.
FIXColumn-toggle checkbox stuck checked
- Symptom. Opening the columns dropdown and clicking a checkbox correctly showed or hid that column, but the checkbox itself stayed in its initial state — visible columns looked permanently checked, so the menu appeared broken even though it worked.
- Cause. The one delegated
clicklistener that routes alldata-bg-actionelements ended with an unconditionale.preventDefault(). Harmless for the anchor- and button-based actions (pagination, sort, refresh), but the column toggle is an<input type="checkbox">— and cancelling a checkbox's click reverts the native toggle and suppresses itschangeevent. - Fix. The handler now skips
preventDefaultfor checkbox / radio controls, where the native toggle is the behavior we want. Row-selection checkboxes were never affected — they route through separate delegates that never cancelled the event.
MIGRATIONComing from 2.5.2?
- Pure drop-in fix. No API change, no markup change. The columns dropdown checkbox now reflects each column's visibility correctly.
- Bundle: 16.7 KB brotli, effectively unchanged (the fix adds a small type guard). Hard ceiling stays at 18 KB.
Host-page dependency guard. A reported docs gap
closes today: nothing in README or on the docs site
told visitors that the host page also needs
Bootstrap's JS bundle (for the toolbar dropdowns)
and an icon font (for the sort / search / refresh
glyphs). Both omissions were silent —
invisible icons, dropdowns that look ready but never
open, no console error. The Getting started area now
spells out the four required loads in order, and the
toolbar warns at first mount when
window.bootstrap.Dropdown is missing.
FIXBootstrap-JS-missing warn at first toolbar mount
- Symptom. Page-size picker and column-visibility menu rendered but never opened on host pages that loaded Boostgrid without Bootstrap's JS bundle. No console error, no exception — the user's most likely reaction was to assume their integration was wrong, not that a documented dependency was missing from the docs.
- Fix.
renderToolbarnow emits a one-shotconsole.warnat first top-toolbar mount whenwindow.bootstrap?.Dropdownis undefined. The message names the missing dependency and includes the exact CDN URL to add. One-shot at module scope so multi-grid pages don't spam the console. - Skipped in non-browser contexts (SSR, vitest workers that don't define
window) — the existing test suite is unaffected.
DOCSHost-page requirements, spelled out
- New Host-page requirements section in the docs Getting started tab (
§ 01.2), with a copy-pasteable four-load CDN snippet in the canonical order: Bootstrap 5 CSS → Bootstrap 5 JS → icon font → Boostgrid. Includes an explicit callout that the JS-missing case is a silent failure and that 2.5.2 surfaces it via the new warn. - README "Host-page requirements" section mirrors the docs site copy and links bidirectionally with the Install section so newcomers can't miss it.
- Minimal markup demoted from
§ 01.2to§ 01.3to make room. Old anchor URLs (#docs-options, etc.) are unaffected — only the in-page section numbers moved.
MIGRATIONComing from 2.5.1?
- Drop-in for working integrations. If your host page already loads Bootstrap 5 JS + an icon font, nothing changes. The new warn only fires when those dependencies are absent.
- If your console suddenly logs
[boostgrid] Bootstrap's JS bundle is not loaded…, add the Bootstrap JS CDN script shown in Getting started → Host-page requirements. - Bundle: 16.41 KB → 16.7 KB brotli (+~290 B for the dependency-detection guard). Hard ceiling stays at 18 KB.
Three reported issues — one CDN-blocking bug, one
silent footgun, and a docs gap — closed in one
metadata-and-docs patch. Shipped JavaScript is
byte-identical to 2.5.0; the change is in how
jsdelivr / unpkg resolve the package entry point
(so the CDN snippet from the docs actually loads),
in a new console.warn that surfaces a
long-silent option-drop, and in a new Ajax wire
format reference on the docs site.
FIXCDN script tag blocked by strict-MIME browsers
- Symptom. The advertised CDN snippet
<script src="https://cdn.jsdelivr.net/npm/boostgrid@2"></script>was refused by Chrome, Firefox, and Edge with "MIME type ('application/node') is not executable". The browser-root URL resolved throughpackage.jsonmaintodist/boostgrid.umd.cjs; jsdelivr and unpkg serve.cjswithContent-Type: application/node. - Fix. Vite now emits a mirrored
dist/boostgrid.umd.js(byte-identical to the.cjs) alongside the original.main+ newbrowser/unpkg/jsdelivrfields + anexports[".default"]condition all point at the.umd.js, so every CDN resolution path lands on a file that serves asapplication/javascript. - Node consumers unchanged. The
exports.requirecondition still resolves to.umd.cjs, which Node needs because the package has"type": "module"and would otherwise try to parse the UMD bundle as ESM.
FIXSilent option-drop when auto-init runs before explicit attach
- Symptom. A
<table data-toggle="boostgrid">auto-init ran atDOMContentLoadedwith defaults; a laterattach('#myGrid', { ajax: true, formatters: { ... } })returned the existing default-options instance with the new options silently dropped. The only way to discover this was by readingindex.ts. - Fix.
attach()now emits aconsole.warnwhen called with non-emptyoptionsagainst an already-attached table, naming the table id and pointing at the most likely cause (thedata-toggleattribute). The runtime behavior is otherwise unchanged. - Docs. A heads-up callout in Getting started → Minimal markup explains the auto-init vs. explicit attach precedence so the conflict is visible before the user trips on it.
DOCSAjax wire format + OPTIONS table polish
- New Ajax wire format section in the Documentation tab (
§ 03.4) lists every field ofAjaxRequestandAjaxResponsewith type and when-sent semantics. Server-side adopters no longer have to grepcore.tsto figure out the canonical payload shape. - Seven missing options added:
highlightRows,ajaxSettings,requestHandler,responseHandler,converters,formatters,performanceMarks. A duplicatevirtualScrollrow was removed. - Sharpened descriptions for the two most-confusing option names.
rowCountnow explicitly flags that it is notrowsPerPage(the DataTables name people reach for).caseSensitivenow explicitly notes it is the top-level option, not nested insidesearchSettings.
MIGRATIONComing from 2.5.0?
- Drop-in. No code changes required. The shipped JavaScript bundle is byte-identical to 2.5.0; only build artifact filenames and
package.jsonmetadata moved. - Update your CDN snippet to the explicit
.umd.jspath:https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.umd.js. The package-root URL (boostgrid@2with no path) now also resolves correctly thanks to themainflip, but the explicit path is recommended for clarity. - If you mix
data-toggle="boostgrid"and explicitattach(): you'll now see aconsole.warnin the browser console. Either drop the attribute (recommended) or move your options into HTMLdata-*attributes on the<table>. - Bundle: 16.25 KB → 16.41 KB brotli (+~160 B for the new
console.warnblock inattach()). Hard ceiling stays at 18 KB.
Focused minor. Element pooling lands under virtual
scroll: the data <tr>s between the
pad rows become a fixed-capacity pool, rebound to new
row payloads in place instead of rebuilt from scratch
on every scroll tick. Per-cell work drops from
"create td + set class + set style + paint" to just
"paint" — an expected 5–10× scroll speed-up.
Shipped alongside a small new public method,
scrollToRow(index), the first index-based
navigation method on the grid. Bundle ticks to
16.25 KB brotli, well under the 18 KB ceiling.
PERFElement pool under virtual scroll
- Recycle, don't rebuild. The windowed body render no longer calls
clearChildren(tbody)on every scroll tick. The data<tr>s already in the DOM become a pool: on each render pass, the pool is grown if the window widened, shrunk if it narrowed, and every row in it is rebound to its new payload via the pre-resolvedpaint[]closures from 2.4.3. Row<tr>identities stay stable across scroll — preserving any focus inside the body and any DOM state the user attached. - Marker class for safety. Pool-built rows carry a
boostgrid-pool-rowclass so the pool can tell its own recyclable rows apart from raw HTML leftovers, the "no results" row, or rows built by a prior tree / group render in the same tbody. - Selection & cell-selection reapplied once per pass. The
table-activeclass on selected rows and the cell-selection rectangle are reapplied after the rebind pass via the existingrefreshSelectionVisuals/paintCellSelectionhelpers — not per-row inside the loop. Selection state survives recycle cleanly. - Pool path is virtual-flat-list only. Tree mode, groupBy, and row-detail rendering paths return earlier in
renderBodyas before and are unaffected. The existing pad-only fast path from 2.4.3 still fires first when the slice didn't move at all.
FEATUREscrollToRow(index)
- New public method.
grid.scrollToRow(index)scrolls the virtual viewport socurrentRows[index]becomes visible. Index is into the current filtered + sorted view (same shape asgetCurrentRows()). Clamped to[0, total - 1]. No-op whenvirtualScrollis off or the dataset is empty. Returnsthisfor chaining. - Synchronous. The window refresh and re-render both happen before the call returns, so callers can immediately query the rendered DOM — e.g. flash a row that just got jumped to.
- First index-based navigation on the grid. All prior navigation was by row id or page number. A
scrollToRow(rowId)overload is queued for a follow-up patch.
PERFActive edit committed before recycle
- Mid-edit scrolls no longer lose data. Before 2.5.0, scrolling while a cell edit was open silently destroyed the edit when the body rebuilt. The pool path now invokes a
commitActiveEdithook before rebinding any row, so the in-flight value is silently committed (same semantics as opening another edit). - Full-rebuild path keeps prior semantics for now. Wiring the same hook into the non-virtual full-rebuild path is a one-liner but was scoped out to keep this round tight.
MIGRATIONComing from 2.4.3?
- Drop-in for everything you were already doing. No behavior change for documented calls; existing virtual-scroll grids automatically benefit from the pool.
- One new method.
scrollToRow(index)is the only public type-surface addition. Two internal hooks (commitActiveEdit,rerenderSelectionState) are present on the instance but are intentionally undocumented. - Bundle: 15.68 KB → 16.25 KB brotli (+~570 B). Hard ceiling stays at 18 KB.
- Tests: 144 → 153 specs (+9 in a new
element poolblock).
Polish trio. Three small, independent perf wins built on
2.4.2: the column-visibility menu now flips
hidden on existing cells in place;
the cell-paint branch is hoisted out of the inner cell
loop into a per-render closure array; and the virtual
scroll skips body rebuild when only the pad heights
changed. No public API surface change. Bundle ticks
to 15.68 KB brotli, still well under the 18 KB ceiling.
PERFDiffed column-visibility toggle
- Hide / show flips
hiddenin place. Toggling a non-frozen column from the visibility menu used to firerenderHeader() + renderBody() + renderFooter()— three full DOM rebuilds. Now onequerySelectorAllper toggle, then a boolean attribute flip on each matching<th>/<td>. Wins scale with row count. - Frozen columns still take the full path. Hiding a frozen column shifts its siblings' sticky offsets, which need the cached offset arrays to recompute. Most users don't hide frozen columns.
- Caveat (intentional). Colspan'd rows (group headers, "no results", master/detail panels) keep their previous colspan until the next full render. The visual overshoot is one column wide and harmless.
PERFPre-resolved cell-paint pipeline
- Branch hoisted out of the cell loop. The inner per-cell
col.formatter ? td.innerHTML = … : td.textContent = …branch fires N rows × M cols times per render. Now apaint[]array of closures is resolved once per column at the top ofrenderBody; the cell loop just callspaint[i](td, row). - Closures capture the column reference. Tested specifically (the bug-prone case is closing over the loop index) so formatter columns keep their per-row data correct.
- Tree-mode caret cells stay on the legacy composition path — their caret + label structure doesn't fit the simple paint signature.
PERFVirtual scroll: pad-only fast path
- New rows + same window = pad mutation only. When ajax delivers more rows but the user is scrolled at the top so the visible slice didn't move, only the pad
<tr>s'style.heightis updated. The data<tr>s keep their identity — preserving any active cell-selection rectangle and any focus the user had inside the body. - New private snapshot field.
grid.lastRenderedVirtualWindowholds the previous frame'sVirtualWindowso the next render can compare and decide whether to rebuild. Cleared ondestroy()and on virtual-scroll teardown.
MIGRATIONComing from 2.4.2?
- Drop-in. No public API additions, no behavior change for any documented call.
- Bundle: 15.46 KB → 15.68 KB brotli (+~220 B for the three diff branches and the snapshot field). Hard ceiling stays at 18 KB.
- Tests: 140 → 144 specs (+4 covering the new code paths).
Performance follow-up to 2.4.1. Single-row selection
toggles only touch the affected <tr>;
column-resize drag is rAF-throttled with a cached cell
list; new opt-in performanceMarks emits
User Timing entries so you can profile renders in
Chrome DevTools. Drop-in patch — no public API
surface change beyond the new option.
PERFDiffed selection toggle
- Single-row select / deselect updates only the affected row. Calling
grid.select([id])or clicking a row checkbox used to walk every visible<tr>viarefreshSelectionVisuals, even though only one row's state changed.select(rowIds)/deselect(rowIds)now route through a newrefreshSelectionForIds()fast path that mutates exactly the changed rows. Bigger wins on bigger pages. - Select-all / deselect-all keep the full-table path. When the caller passes no
rowIds, we fall through to the existing whole-table refresh — every row legitimately changed. - The header
select-allcheckbox is re-evaluated every call (cheapevery()overcurrentRows) so it stays in sync.
PERFColumn-resize live sync
- Pre-resolved matching
<td>list. The live width sync used to callquerySelectorAll("tbody > tr > td")and iterate it on every pointermove. Rows can't change mid-drag, so we now resolve the matching cells once at drag start and reuse them. - rAF-throttled. High-frame-rate pointers fire
mousemove120-240 Hz; we only need one width write per animation frame. The latest cursor X wins; intermediates are coalesced. - Synchronous flush on mouseup. If
mouseuparrives before the next rAF tick, we apply the pending move sync so the final committed width is exact.
FEATUREPerformance marks (opt-in)
- New option:
performanceMarks: true. When on, everyrenderHeader/renderBody/renderFooterpass emitsperformance.mark()+performance.measure()entries via the User Timing API. Mark name namespaced asboostgrid:<table-id>:<phase>. - Profile in production. Open Chrome DevTools → Performance → Record → interact with the grid → the entries surface under the Timings track. Lets app authors find slow formatters or measure the impact of their own customizations.
- Zero overhead by default. Off unless explicitly enabled — even
performance.markcalls add up at 60 Hz scroll, so we don't pay for what most users don't profile.
MIGRATIONComing from 2.4.1?
- Drop-in. No behavior changes for code that doesn't use
performanceMarks. - Selection events unchanged.
onSelected/onDeselectedstill fire with the same row arrays in the same order. - Bundle: 15.09 KB → 15.46 KB brotli (+~370 B). Hard ceiling stays at 18 KB.
Stability + performance patch. Out-of-order ajax responses can no longer overwrite fresher ones; the cell-edit listener leak is closed; render hot paths are cached for a 15–30 % speed-up on large datasets; state writes are debounced. No public API changes — drop-in replacement for 2.4.0.
FIXStability
- AJAX request sequencing. Rapid search typing or sort clicks used to fire overlapping fetches; if the later request resolved first the older response would silently overwrite it. A monotonic
ajaxRequestId+AbortControllerdrops stale responses and frees the network slot.destroy()aborts in-flight requests too. - Cell-edit listener cleanup. The inline
<input>'skeydown+blurhandlers were never removed when the editor closed. Setting the cell'sinnerHTMLdispatched a synthetic blur on the now-detached input which re-enteredcommit(). Fixed via explicitdetach()before swapping the input out.
PERFRender hot paths
- Frozen-offset arrays cached per render. The legacy
frozenLeftPx/frozenRightPxwalked the visible-columns array per cell, per row — quadratic in column count. Replaced bycomputeFrozenOffsets(), a single O(n) pass at render entry. 50 cols × 100 rows = 5000 walks → 1. - Selection refresh via row-index Map.
refreshSelectionVisualsusedcurrentRows.find()per visible row; now reads from the existingrowIndex: Map<id, Row>in O(1). - Search regex compiled once per phrase. Was rebuilt inside
applyFilteron every call; cached ongrid.searchRegexand invalidated whenever the phrase changes. - Sort comparator cached.
applySortrebuiltObject.entries(sortDictionary)+ a closure each call; now compiled once and reused until the sort dictionary changes. - Visible-cols + columnById Maps memoized per render. Eliminates repeated
.filter()and.find()walks in the body / group / footer / column-visibility paths. - Tree
markUpdedup. Walking ancestors per matched leaf used to re-walk the chain N times for N matching leaves; now short-circuits once an ancestor is in the seen-set. - Virtual scroll: skip render when the slice is unchanged. Small scrolls within the overscan window no longer re-render the body. The
requestAnimationFramecoalescer remains. - Virtual length read direct. The scroll handler used to call
getFilteredRows().length, which sliced the entire filtered array every event. Now reads the internallengthdirectly.
PERFState persistence debounced
- 200 ms trailing-edge debounce. A column-resize drag at 60 Hz no longer pays
JSON.stringifyper pointermove — writes coalesce into a single localStorage hit at the end of the burst. - New
grid.flushState()helper. Synchronously writes any pending save. Useful inbeforeunloadhandlers and tests that readlocalStoragemid-flight.destroy()flushes automatically.
TESTCoverage
- +7 specs in
test/quality.test.tscovering: out-of-order ajax → newest wins, destroy-during-fetch is silent, stale-input keydown is a no-op, debounce coalescing,destroy()flush, invalid BCP-47 locale doesn't crash, doubledestroy()is a no-op. - Total: 151 specs across 4 packages (was 144).
MIGRATIONComing from 2.4.0?
- Drop-in. No public API changes; all options, methods, and events keep their signatures.
- One additive method:
grid.flushState()for code that needs synchronous state writes. Optional. - State save is now async. Reads of
localStorageimmediately after a mutation may not see the latest payload until ~200 ms later. Callgrid.flushState()first if you need to read it sync (rare outside tests). - Bundle ceiling raised 15 KB → 18 KB brotli to make room for the AJAX-abort plumbing + frozen-offset arrays. Current size: 15.09 KB. The hard ceiling is enforced by
size-limit.
i18n & polish. Every UI string is now in
options.labels; pagination summaries
digit-group via Intl.NumberFormat; opt-in
sticky <thead>; auto-tooltips on
clipped cells. Bundle unchanged at 14.46 KB.
FEATURERound 7 — i18n hardening
- 14 new label keys covering every UI string introduced in 2.0–2.3 — the column-visibility panel, master/detail chevrons, bulk-action bar count + clear button, tree carets, drag-resize affordances. The legacy keys (
all,infos,loading,noResults,refresh,search) are unchanged. - Locale-aware digit grouping. Pass
locale: "de-DE"(or any BCP-47 tag) and the “Showing N entries” line usesIntl.NumberFormatfor thousands separators. Cached per locale so we don't allocate a formatter per render. - Override any subset. Missing keys fall back to English, so a partial translation works fine while you complete it.
FEATURERound 7 — Polish
- Sticky header.
stickyHeader: truepins<thead>viaposition: sticky. Coexists with frozen columns — z-indexes are arranged so the corner cell wins. Override the offset for fixed-nav layouts via--boostgrid-sticky-top: 56px. - Truncated-cell tooltips. A delegated
mouseoverlistener lazily attaches atitleattribute when a cell'sscrollWidth > clientWidth. Skips cells that already have a title or carrydata-bg-no-tooltip. On by default;truncatedTooltips: falseopts out.
MIGRATIONComing from 2.3?
- No breaking changes. The new label keys ship with English defaults; existing translations keep working.
- Net-zero bundle impact — the i18n routing replaced inline string literals that were already in the build.
stickyHeaderis opt-in to avoid surprising layouts on grids embedded inside small overflow containers.
Power-user UX & server-side. Master/detail row panels, spreadsheet-style cell selection with TSV copy, a sticky bulk-action bar, animated loading skeleton, and an ajax payload that surfaces grouping + tree state for server-driven grids.
FEATURERound 6 — Power-user UX
- Master/detail row expansion.
rowDetail: (row) => string | HTMLElementrenders an expand chevron in a leading affordance cell. The colspan'd panel below the row hosts whatever you return — returnnullto skip a row's panel. - Cell selection & clipboard.
cellSelection: trueturns the body into a spreadsheet — click anchors, shift-click or drag extends the rectangle, Ctrl/Cmd+C copies as tab-separated values, Esc clears. - Bulk-action bar.
bulkActionsrenders a sticky toolbar that slides in whenever selection is non-empty, with an auto-prepended count and "Clear" button. - Loading skeleton. Animated placeholder rows replace the blank body during ajax fetches by default. Numeric override or
loadingSkeleton: falsefor the previous behaviour.
FEATURERound 6 — Server-side adapters
- AjaxRequest extensions.
groupBy+collapsedGroupsare sent on every fetch when grouping is on;treeMode+expandedTreeNodeswhen the tree is on. Single-stringgroupByis normalized to an array. Existing endpoints see the same payload they always did — new fields appear only when their feature is active. - Adapter sketch. Worked Express example demonstrates how to switch query shape based on the request payload: paginated flat, group-sorted, or root-plus-expanded-children for tree.
MIGRATIONComing from 2.2?
- No breaking changes.
cellSelection,rowDetail,bulkActionsare all opt-in. The ajax extensions only appear in the request payload when their feature is on. loadingSkeletondefaults totrue— set it tofalseif you have a custom in-flight UI you'd rather keep.cellSelectionconflicts withrowSelect(the whole-row click handler). Pick one.
Column UX & tree polish. Drag-reorder + drag-resize on every column, frozen-right pinning, a richer column-visibility panel, drag-to-reparent on tree rows, and tree-aware export indentation.
FEATURERound 5 — Column UX
- Frozen-right columns.
frozen: "right"pins a column to the trailing edge during horizontal scroll. The shadow now reads from both directions when frozen prefixes and suffixes are present. - Column reorder. Drag any header to rearrange. Frozen-group constrained — dropping a left-frozen column onto a non-frozen target snaps to the end of the frozen run. Persisted in v:3 state.
- Column resize. Drag the right edge of a header to resize. Live-updates body cells without a full re-render.
minWidth/maxWidthper column; persisted ascolumnWidths. - Visibility panel polish. Search input, drag-handles for reorder, "Reset to defaults" button. The panel is also a reorder UI — pulling the same
reorderColumn()backend.
FEATURERound 5 — Hierarchical polish
- Tree drag-to-reparent. Opt-in via
treeReparent: true. Drop a row onto another to changeparentId. Cycle-guarded;onReparentcan return a Promise that resolvesfalseto abort. - Subtotal-on-top option.
groupSubtotalsOnTop: trueemits group footers above their member rows, applied at every grouping level uniformly. - Tree-aware export. The export plugin's new
treeExportoption indents the tree-column cell by depth, or inserts a leadingPathcolumn with slash-joined ancestors. Zero core cost.
MIGRATIONComing from 2.1?
- No breaking changes. Reorder + resize default to on — opt out via
columnReorder: falseorcolumnResize: false, or per-column withreorderable: false/resizable: false. - State schema bumped from v:2 to v:3 to carry
columnOrderandcolumnWidths. v:2 payloads still apply non-column fields cleanly. treeReparentstays off by default — it mutates user data, so it asks for explicit consent.
Hierarchical data, end to end. Multi-level grouping closes the depth gap left in 2.0; tree mode ships adjacency-list parent/child rows with ancestor-aware search.
FEATURERound 4 — Hierarchical data
- Multi-level grouping.
groupBynow acceptsstring | string[]; nested headers indent per depth, andgroupAggregatorscan target a specific tier with thecolId@Nkey syntax. - Tree data mode. Set
treeMode: truewith aparentIdfield on each row — Boostgrid reconstructs the tree, renders DFS with depth-driven indentation, and exposestoggleTreeNode/expandAllTree/collapseAllTree. - Ancestor-aware search. When the search input matches a deeply-nested leaf, its entire ancestor chain stays visible so the path to the hit is never hidden behind a collapsed parent.
- State persistence v:2. Schema bumped from v:1; collapsed group paths and expanded tree nodes round-trip via
localStorage. v:1 payloads still apply non-hierarchical fields cleanly. - Cycle & orphan defense. Tree builder warns and breaks cycles, promotes orphan rows to roots — never crashes on malformed input.
MIGRATIONComing from 2.0?
- No breaking changes. Existing
groupBy: "status"code keeps working — the array form is purely additive. - If you persisted state in 2.0, that v:1 payload still applies; new collapse/expand state starts fresh under v:2.
- The
GroupContextshape gainedgroupPath+depth; existing aggregators that read onlyrows/groupKey/groupLabelneed no change.
A complete vanilla TypeScript rewrite. Zero runtime dependencies, ESM + UMD, ~9 KB brotli, three framework wrappers and an export plugin in lockstep.
FEATURERound 3 — Power features
- Virtual scroll. Opt-in via
virtualScroll: true. Windowed rendering with rAF coalescing — sustained ten thousand rows at sixty frames. - Cell-level edit. Per-column
editablewithtext / number / selecteditors,onCellEditcommit hook, andrevert()for async-server rollback. - Row grouping with subtotals.
groupBy+groupAggregatorsrender group headers, member rows, and per-group footers. Collapse state persists. - Frozen left columns. Sticky positioning with cumulative left offsets and a scroll-shadow that fades in only after
scrollLeft > 0. boostgrid-exportplugin. CSV (RFC 4180), Excel via lazyxlsx-js-style, and Print. Zero bytes added to the core bundle.vue-boostgridwrapper. Vue 3 +<script setup>; same imperative handle as the React wrapper.
FEATURERound 2 — Type safety & state
- Generic over
TRow.Boostgrid<TRow>threads your row shape through every formatter, callback, footer and edit hook. Full inference end-to-end. - State persistence.
stateSave: true— sort, search, page, rows-per-page, selection, column visibility and collapsed groups round-trip via localStorage with a versioned schema. - Footer aggregation. DataTables-style
footer: truewith per-columnfooterFormatteror whole-rowfooterCallback.FooterContextexposes filtered + current-page row sets.
BREAKINGRound 1 — The rewrite
- jQuery removed. The library is now vanilla TypeScript. The
$.fn.bootgrid()entry point is gone; useattach()ordata-toggle="boostgrid". - Modern packaging. ESM + UMD output, side-effect-free, tree-shakeable formatters.
exportsmap for first-class subpath imports. - Bootstrap 5 native. Pagination, toolbar and badges use BS5 classes and CSS variables. No bespoke theming layer.
- React wrapper.
react-boostgridships in the same monorepo. Host-div pattern keeps the reconciler off the table DOM. - Single delegated listener. One click handler per grid; row buttons route through
data-bg-action. - Bundle ceiling. 15 KB brotli enforced by
size-limit. Currently ~9 KB.
MIGRATIONComing from 1.x?
- Replace
$("#grid").bootgrid({…})withattach("#grid", {…}). - Column option names are kebab-case in markup (
data-column-id,data-identifier) and camelCase in JavaScript (columnId,identifier) — same as before. - Custom formatters are now plain functions
(value, row) => string; nothisbinding needed. - The
loaded,selected,deselectedevents still fire as DOM events; you can also usegrid.on("selected", …).
The original was jquery-bootgrid: a Bootstrap 3 / 4 table plugin in jQuery. It served well for nearly a decade. Boostgrid 2 is the spiritual successor — same data-attribute ergonomics, modern internals.
The 1.x line is no longer maintained. Source remains available on the legacy GitHub repository for archival reference. New projects should start on 2.x.
Items still ahead — listed here so adopters can see where the road goes, not as promises. Frozen-right, column reorder + drag-resize, and tree drag-to-reparent all shipped in 2.2.
- Variable-row-height virtual scroll (measure-on-mount).
- PDF export.
- Accessibility: full ARIA grid pattern + keyboard navigation.
- i18n — extracted strings, locale-aware formatters.
- Web-Worker sort/filter for very large in-memory datasets.
Colophon
about the maintainerA letter from the maintainer
Hi, my name is Jeffery Leo.
I built Boostgrid—a modern, Bootstrap 5-native data grid for the modern browser. By day I work on developer tooling, .NET back-ends, and modern web front-ends.
Boostgrid is a typed, vanilla, tree-shakeable package that drops into any framework. No jQuery, no runtime dependencies, under 18 KB brotli, and a public API that stays out of your way.
The project is permissively licensed, public on GitHub, and accepts contributions of every shape. If something irks you, open an issue and tell me about it; I read every one.
— Jeffery written in visual studio · 2026