Boostgrid
↳ chapter 01 / getting started issue 02 · spring edition

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
§ 01.1

Install

three ways in
cdn.jsdelivr.net
// 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">
~ / your-project
$ npm install boostgrid

import { attach } from "boostgrid";
import "boostgrid/style.css";

attach("#grid");
PM>
// .NET / Visual Studio
PM> Install-Package Boostgrid

// drops boostgrid.min.{js,css}
// into ~/Scripts and ~/Content
§ 01.2

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

<head> — minimum load order
<!-- 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>
§ 01.3

Minimal markup

html + data-attributes

Mark up a regular Bootstrap 5 table, then either set data-toggle="boostgrid" for auto-init or call attach().

index.html
<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>
§ 02

Examples

live, copy-pastable
— select an example —

Pick an example on the left to see it run with the real bundle.

↳ chapter 03 / reference api surface

The full specification.

§ 03.1

Options

passed to new Boostgrid(t, …)
OptionTypeDefaultDescription
§ 03.2

Methods

on a Boostgrid instance
MethodReturnsDescription
§ 03.3

Events

DOM events + typed grid.on()
EventPayloadDescription
§ 03.4

Ajax wire format

when ajax: 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

FieldTypeDescription

RESPONSEAjaxResponse

FieldTypeDescription
§ 03.5

Performance

why it is fast
  1. 01

    Single delegated listener

    One click handler per grid; row buttons route through data-bg-action—no per-row bindings to rebuild on render.

  2. 02

    Memoized derived view

    Filter, sort and pagination are independent layers. Changing pages doesn't re-sort; sorting doesn't re-filter.

  3. 03

    O(1) selection lookups

    A Map<id, Row> is built once on data load—select(), remove() and get are constant-time.

  4. 04

    Debounced search

    The search input coalesces keystrokes (200 ms by default) so the filter pipeline doesn't run on every letter.

  5. 05

    CSS containment

    The grid root carries contain: content; rows use contain: layout style—paint and layout stay inside the grid subtree.

  6. 06

    Bundle guarded at < 18 KB

    Every PR runs size-limit. Current ship size is 16.7 KB brotli — numbers tracked against the actual build, not estimated.

↳ chapter 04 / changelog release notes · reverse-chronological

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.

02.5.3 current
· patch release

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 click listener that routes all data-bg-action elements ended with an unconditional e.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 its change event.
  • Fix. The handler now skips preventDefault for 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.
02.5.2 previous
· patch release

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. renderToolbar now emits a one-shot console.warn at first top-toolbar mount when window.bootstrap?.Dropdown is 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.2 to § 01.3 to 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.
02.5.1 previous
· patch release

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 through package.json main to dist/boostgrid.umd.cjs; jsdelivr and unpkg serve .cjs with Content-Type: application/node.
  • Fix. Vite now emits a mirrored dist/boostgrid.umd.js (byte-identical to the .cjs) alongside the original. main + new browser / unpkg / jsdelivr fields + an exports[".default"] condition all point at the .umd.js, so every CDN resolution path lands on a file that serves as application/javascript.
  • Node consumers unchanged. The exports.require condition 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 at DOMContentLoaded with defaults; a later attach('#myGrid', { ajax: true, formatters: { ... } }) returned the existing default-options instance with the new options silently dropped. The only way to discover this was by reading index.ts.
  • Fix. attach() now emits a console.warn when called with non-empty options against an already-attached table, naming the table id and pointing at the most likely cause (the data-toggle attribute). 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 of AjaxRequest and AjaxResponse with type and when-sent semantics. Server-side adopters no longer have to grep core.ts to figure out the canonical payload shape.
  • Seven missing options added: highlightRows, ajaxSettings, requestHandler, responseHandler, converters, formatters, performanceMarks. A duplicate virtualScroll row was removed.
  • Sharpened descriptions for the two most-confusing option names. rowCount now explicitly flags that it is not rowsPerPage (the DataTables name people reach for). caseSensitive now explicitly notes it is the top-level option, not nested inside searchSettings.

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.json metadata moved.
  • Update your CDN snippet to the explicit .umd.js path: https://cdn.jsdelivr.net/npm/boostgrid@2/dist/boostgrid.umd.js. The package-root URL (boostgrid@2 with no path) now also resolves correctly thanks to the main flip, but the explicit path is recommended for clarity.
  • If you mix data-toggle="boostgrid" and explicit attach(): you'll now see a console.warn in the browser console. Either drop the attribute (recommended) or move your options into HTML data-* attributes on the <table>.
  • Bundle: 16.25 KB → 16.41 KB brotli (+~160 B for the new console.warn block in attach()). Hard ceiling stays at 18 KB.
02.5.0 previous
· minor release

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-resolved paint[] 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-row class 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-active class on selected rows and the cell-selection rectangle are reapplied after the rebind pass via the existing refreshSelectionVisuals / paintCellSelection helpers — 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 renderBody as 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 so currentRows[index] becomes visible. Index is into the current filtered + sorted view (same shape as getCurrentRows()). Clamped to [0, total - 1]. No-op when virtualScroll is off or the dataset is empty. Returns this for 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 commitActiveEdit hook 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 pool block).
02.4.3 previous
· patch release

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 hidden in place. Toggling a non-frozen column from the visibility menu used to fire renderHeader() + renderBody() + renderFooter() — three full DOM rebuilds. Now one querySelectorAll per 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 a paint[] array of closures is resolved once per column at the top of renderBody; the cell loop just calls paint[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.height is 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.lastRenderedVirtualWindow holds the previous frame's VirtualWindow so the next render can compare and decide whether to rebuild. Cleared on destroy() 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).
02.4.2 previous
· patch release

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> via refreshSelectionVisuals, even though only one row's state changed. select(rowIds) / deselect(rowIds) now route through a new refreshSelectionForIds() 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-all checkbox is re-evaluated every call (cheap every() over currentRows) so it stays in sync.

PERFColumn-resize live sync

  • Pre-resolved matching <td> list. The live width sync used to call querySelectorAll("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 mousemove 120-240 Hz; we only need one width write per animation frame. The latest cursor X wins; intermediates are coalesced.
  • Synchronous flush on mouseup. If mouseup arrives 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, every renderHeader / renderBody / renderFooter pass emits performance.mark() + performance.measure() entries via the User Timing API. Mark name namespaced as boostgrid:<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.mark calls 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 / onDeselected still 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.
02.4.1 previous
· patch release

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 + AbortController drops stale responses and frees the network slot. destroy() aborts in-flight requests too.
  • Cell-edit listener cleanup. The inline <input>'s keydown + blur handlers were never removed when the editor closed. Setting the cell's innerHTML dispatched a synthetic blur on the now-detached input which re-entered commit(). Fixed via explicit detach() before swapping the input out.

PERFRender hot paths

  • Frozen-offset arrays cached per render. The legacy frozenLeftPx / frozenRightPx walked the visible-columns array per cell, per row — quadratic in column count. Replaced by computeFrozenOffsets(), a single O(n) pass at render entry. 50 cols × 100 rows = 5000 walks → 1.
  • Selection refresh via row-index Map. refreshSelectionVisuals used currentRows.find() per visible row; now reads from the existing rowIndex: Map<id, Row> in O(1).
  • Search regex compiled once per phrase. Was rebuilt inside applyFilter on every call; cached on grid.searchRegex and invalidated whenever the phrase changes.
  • Sort comparator cached. applySort rebuilt Object.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 markUp dedup. 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 requestAnimationFrame coalescer remains.
  • Virtual length read direct. The scroll handler used to call getFilteredRows().length, which sliced the entire filtered array every event. Now reads the internal length directly.

PERFState persistence debounced

  • 200 ms trailing-edge debounce. A column-resize drag at 60 Hz no longer pays JSON.stringify per pointermove — writes coalesce into a single localStorage hit at the end of the burst.
  • New grid.flushState() helper. Synchronously writes any pending save. Useful in beforeunload handlers and tests that read localStorage mid-flight. destroy() flushes automatically.

TESTCoverage

  • +7 specs in test/quality.test.ts covering: 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, double destroy() 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 localStorage immediately after a mutation may not see the latest payload until ~200 ms later. Call grid.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.
02.4.0 previous
· minor release

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 uses Intl.NumberFormat for 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: true pins <thead> via position: 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 mouseover listener lazily attaches a title attribute when a cell's scrollWidth > clientWidth. Skips cells that already have a title or carry data-bg-no-tooltip. On by default; truncatedTooltips: false opts 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.
  • stickyHeader is opt-in to avoid surprising layouts on grids embedded inside small overflow containers.
02.3.0 previous
· minor release

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 | HTMLElement renders an expand chevron in a leading affordance cell. The colspan'd panel below the row hosts whatever you return — return null to skip a row's panel.
  • Cell selection & clipboard. cellSelection: true turns 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. bulkActions renders 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: false for the previous behaviour.

FEATURERound 6 — Server-side adapters

  • AjaxRequest extensions. groupBy + collapsedGroups are sent on every fetch when grouping is on; treeMode + expandedTreeNodes when the tree is on. Single-string groupBy is 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, bulkActions are all opt-in. The ajax extensions only appear in the request payload when their feature is on.
  • loadingSkeleton defaults to true — set it to false if you have a custom in-flight UI you'd rather keep.
  • cellSelection conflicts with rowSelect (the whole-row click handler). Pick one.
02.2.0 previous
· minor release

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 / maxWidth per column; persisted as columnWidths.
  • 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 change parentId. Cycle-guarded; onReparent can return a Promise that resolves false to abort.
  • Subtotal-on-top option. groupSubtotalsOnTop: true emits group footers above their member rows, applied at every grouping level uniformly.
  • Tree-aware export. The export plugin's new treeExport option indents the tree-column cell by depth, or inserts a leading Path column with slash-joined ancestors. Zero core cost.

MIGRATIONComing from 2.1?

  • No breaking changes. Reorder + resize default to on — opt out via columnReorder: false or columnResize: false, or per-column with reorderable: false / resizable: false.
  • State schema bumped from v:2 to v:3 to carry columnOrder and columnWidths. v:2 payloads still apply non-column fields cleanly.
  • treeReparent stays off by default — it mutates user data, so it asks for explicit consent.
02.1.0 previous
· minor release

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. groupBy now accepts string | string[]; nested headers indent per depth, and groupAggregators can target a specific tier with the colId@N key syntax.
  • Tree data mode. Set treeMode: true with a parentId field on each row — Boostgrid reconstructs the tree, renders DFS with depth-driven indentation, and exposes toggleTreeNode / 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 GroupContext shape gained groupPath + depth; existing aggregators that read only rows / groupKey / groupLabel need no change.
02.0.0 previous
· major release

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 editable with text / number / select editors, onCellEdit commit hook, and revert() for async-server rollback.
  • Row grouping with subtotals. groupBy + groupAggregators render 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-export plugin. CSV (RFC 4180), Excel via lazy xlsx-js-style, and Print. Zero bytes added to the core bundle.
  • vue-boostgrid wrapper. 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: true with per-column footerFormatter or whole-row footerCallback. FooterContext exposes filtered + current-page row sets.

BREAKINGRound 1 — The rewrite

  • jQuery removed. The library is now vanilla TypeScript. The $.fn.bootgrid() entry point is gone; use attach() or data-toggle="boostgrid".
  • Modern packaging. ESM + UMD output, side-effect-free, tree-shakeable formatters. exports map 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-boostgrid ships 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({…}) with attach("#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; no this binding needed.
  • The loaded, selected, deselected events still fire as DOM events; you can also use grid.on("selected", …).
01.x archive
· jquery-bootgrid era

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.

next planned
no commitments · subject to change

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

Colophon

about the maintainer

A 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