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.

  • 5.51kb 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
<script src="https://cdn.jsdelivr.net/npm/boostgrid@2"></script>
<link rel="stylesheet"
      href="…/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

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

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 < 15 KB

    Every PR runs size-limit. We ship at 5.51 KB brotli today—numbers tracked against the 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.4.3 current
· 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 clean, typed, vanilla, tree-shakeable package that drops into any framework. No jQuery, no runtime dependencies, less than 15 KB gzipped, 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