Vanilla-Grid Technical Docs
This folder contains implementation-focused documentation for core Vanilla-Grid features.
Related component: Vanilla Grid Toolbar (
<vn-grid-toolbar>) — a state-aware command surface bound to a grid. It consumes the additive grid API (getSearchTerm(),getSearchFields(),isBusy(), thevn-grid-total-row-count-changedevent).
Documents
Listed in reading order — foundations first, then the features that build on them. See Suggested Reading Order for a shorter path through the set.
1. Core engine
- Web Component Implementation (
<vn-grid>) — element lifecycle, markup generation, API forwarding, event dispatch. - Row Virtualization and Custom Scrollbars — the rendering and scroll engine every other feature sits on.
2. Data access
- DataManager Implementation — the data access contract, the built-in managers, and
<vn-grid>lifecycle wiring. - Row Key Management — stable row identity: key strategies, compound keys, OData considerations.
- Infinite Scroll Implementation — prefetch thresholds, load chaining, known-total resolution.
3. Sorting, filtering, and column types
- Sorting Implementation — single/multi-sort state, comparator chaining, server-sort delegation.
- Column Filters Implementation — filter model, header menu UI, client/server filter dispatch.
- Date, DateTime, and Time Types Implementation — how the three temporal types parse, sort, filter, and render.
4. Row interaction
- Selection Implementation — key resolution, mode semantics, sticky select-all, event payloads.
- Keyboard Navigation Implementation — key event gating, delta computation, momentum guards.
5. Column management
- Column Resizing Implementation — pointer resize math, guide rendering, width persistence.
- Columns Reordering Implementation — drag/drop normalization, touch hold behavior, order persistence.
- Column Visibility Implementation — hide/show projection, guards, refresh behavior.
- Column Freezing Implementation — left-pinned grouping, offset computation, freeze guide rendering.
6. Presentation
- Themes Implementation — how visual systems layer on top of the core behavior.
- Localization Implementation — locale resolution, message dictionaries, number and scroll-indicator formatting.
7. Export
- Export to Excel Implementation — the
exportToExcel()API, the streaming.xlsxwriter and export Worker, progress / Cancel / errors, header label resolution,formatCellusage. - Excel Export Theming Implementation — how an export matches the active theme, and how custom themes declare an export palette.
8. Persistence, delivery, and performance
- Local Storage Settings Implementation — which settings persist and in what JSON format.
- Cache-Bust Strategy — dev vs. build token strategy for feature-script and asset loading.
- Performance Analysis — audit of potential performance improvements, grouped by category and ranked by impact.
9. Row grouping
- Row Grouping Implementation — multi-level client-side grouping: the
renderEntriesrender projection, the canonical path-based group-key encoder, degradation over a partial dataset, persistence, the keyboard/focus contract for caption toggles, and the grouped-column removal + group bar that make a grouped column reachable again.
Architecture
Vanilla-Grid splits responsibilities into feature modules loaded automatically from features/:
| Feature file | Concern |
|---|---|
grid-events.js |
Frozen registry of every CustomEvent name dispatched by <vn-grid> (window.VanillaGridEvents) — loaded first so every other script and the host app can reference VanillaGridEvents.* instead of bare string literals |
dom-scheduler.js |
FastDOM-style read/write batcher (window.VanillaGridScheduler) — coalesces layout reads and DOM writes into one rAF to prevent thrashing |
templates.js |
Cached <template>-cloning helpers (window.VanillaGridTemplates) for the empty / error / initial-empty rows — escape-safe via textContent, no innerHTML interpolation |
intl-cache.js |
Shared Intl.Collator cache (window.VanillaGridIntlCache) — unifies the locale-aware string-comparison cache used by vanilla-grid.js (_getCollator) and sorting.feature.js (_getSortCollator) |
shimmer-threshold.js |
Shared row-count threshold parse/clamp rule (window.VanillaGridShimmerThreshold.resolve) for every "shimmer/worker above N rows" option — used by vanilla-grid.js (sortShimmerThreshold/sortWorkerThreshold), vanilla-grid-element.js (dataLoading.shimmerThreshold/workerThreshold), and static-data-manager.js (filter/search worker threshold) |
main-thread-yield.js |
Shared main-thread yield primitive (window.VanillaGridYield.yieldToMainThread) for long chunked CPU passes — scheduler.yield() → a MessageChannel macrotask → setTimeout, deliberately not requestAnimationFrame (see dom-scheduler.js for the paint-aligned case). Used by static-data-manager.js (filter/search extraction) and sorting-worker.feature.js (per-column sort-key extraction), both with a plain-macrotask fallback when it is absent |
filter-model.js |
Pure, shared filter-model utility (window.VanillaGridFilterModel) — type-aware operator catalog, arity, and normalizeFilterModel/validateOperatorForType/cloneFilterModel. Used by both data managers and the header-menu filter UI |
features/datetime.feature.js |
Shared temporal parser/formatter (window.VanillaGridDateTimeFeature) — the single source of truth for date/datetime/time parsing. Owns the { ok, kind, numeric, raw } parse envelope (explicit null vs invalid), sourceFormat/inputPattern handling, bucketed compareTemporalValues, and the per-column Intl.DateTimeFormat cache. Used by vanilla-grid.js (_formatByType), sorting-comparator.feature.js (both sort paths), static-data-manager.js and odata-data-manager.js |
column-key.js |
Shared column-key normalizer (window.VanillaGridColumnKey.normalizeColumnKey) — the single source of truth for trim/lowercase key comparison. Must load before columns.feature.js and persistence.feature.js, which call it directly with no fallback |
column-access.js |
Shared per-row value helpers (window.VanillaGridColumnAccess) — compileColumnAccessor() (the compiled column._getValue, reused by every render, sort, group-boundary scan and export) and the tri-state coerceBoolean(). Extracted out of vanilla-grid.js so build.js can keep them off the heavy obfuscator profile via BUNDLE_FAST_PATH, which can only exempt whole files; vanilla-grid.js binds both at load time and keeps an inline fallback copy for partial bundles |
aggregate-registry.js |
Named registry of group-aggregate reducers (window.VanillaGridAggregateRegistry) — the built-ins, host reducers added via VanillaGrid.registerAggregate(), and the column.type eligibility gate. Used by grouping.feature.js, header-menu.feature.js and vanilla-grid.js |
storage/grid-storage-provider.js |
Storage provider contract + built-in null/local/remote implementations |
persistence.feature.js |
Settings read/write for widths, order, hidden & frozen columns (sync + async aware, with background write queue) |
selection.feature.js |
Row selection modes, key resolution, sticky select-all |
sorting.feature.js |
Single/multi-sort state, comparator chaining, server-sort delegation (coordinator) |
sorting-comparator.feature.js |
Locale-aware defaultCompareValues + cached Intl.Collator (extends VanillaGridSortingFeature.prototype) |
sorting-worker.feature.js |
Off-thread sort: worker eligibility, worker source/pool, _sortViaWorker (extends VanillaGridSortingFeature.prototype) |
grouping.feature.js |
Multi-level, client-side row grouping (window.VanillaGridGroupingFeature, standalone class like sorting.feature.js) — group-key encoding, the renderEntries projection, collapse state, and the grid-local availability gate |
grouping-bar.feature.js |
The group bar — the grid-owned strip above the column header carrying one chip per applied group level, with its direction control and ungroup action (extends VanillaGrid.prototype; holds no state of its own, re-rendered from getGroupState()) |
columns.feature.js |
Column state coordinator — owns the shared column list/state; visibility, freezing, reordering and resizing are split into the four sub-features below |
columns-visibility.feature.js |
Hide/show columns (extends VanillaGridColumnsFeature.prototype) |
columns-freeze.feature.js |
Column freezing (extends VanillaGridColumnsFeature.prototype) |
columns-reorder.feature.js |
Column reordering (extends VanillaGridColumnsFeature.prototype) |
columns-resize.feature.js |
Column resizing (extends VanillaGridColumnsFeature.prototype) |
stretch-to-fit.feature.js |
Smart, constraint-aware stretch-to-fit width distribution across resizable, non-selection, non-frozen columns (extends VanillaGridColumnsFeature.prototype) |
header-menu.feature.js |
Header right-click / long-press context menu (extends VanillaGridColumnsFeature.prototype) |
header-menu-submenu.feature.js |
The header menu's submenu flyout mechanism — hover/click/keyboard open, arrow traversal, edge flip, RTL (extends VanillaGridColumnsFeature.prototype; must load after header-menu.feature.js) |
rendering.feature.js |
Header markup, virtual row pool, renderVisibleRows, empty/error/skeleton states, row-height CSS var |
viewport.feature.js |
Viewport sizing math, ResizeObserver plumbing, viewport row snap, header/scrollbar alignment |
interaction.feature.js |
Scroll/wheel handlers, custom scrollbar drag, hover & blur tracking (coordinator) |
interaction-pointer.feature.js |
Click / body-checkbox / pointer-down handlers (extends VanillaGrid.prototype) |
interaction-keyboard.feature.js |
Keyboard navigation + key-scroll handlers (extends VanillaGrid.prototype) |
excel-export.feature.js |
Installs exportToExcel(options) on VanillaGrid.prototype (coordinator: rows, columns, styles, the chunked producer, progress, Cancel, errors) |
excel-export-worker.feature.js |
The streaming .xlsx writer, the warm export Worker that runs it, the main-thread fallback, eligibility (shared VanillaGridExcelExportInternals namespace) |
excel-export-delivery.feature.js |
iOS / Web-Share detection, file download, "Exporting…" overlay: progress, Cancel, failure dialog (shared VanillaGridExcelExportInternals namespace) |
data-managers/data-manager.js |
DataManager base contract (fetchRows, buildRequestHeaders, transformRows, handleSort, …) that custom managers extend — see DataManager |
data-managers/odata-data-manager.js |
Built-in ODataDataManager ($skip/$top/$orderby/$filter, separate $count) |
data-managers/graphql-data-manager.js |
Built-in GraphQLDataManager (offset or cursor pagination, inline or separate total) |
data-managers/static-data-manager.js |
Built-in StaticDataManager — client-side sort/filter/search over resident rows, with a filter/search Worker for large datasets |
vanilla-grid.js auto-loads these scripts relative to its own path, so consumers only need a single <script> tag.
Loading strategy
All dependency <script> tags are injected in one synchronous pass with
async = false: the HTML spec guarantees in-order execution while the
downloads proceed fully in parallel, so boot latency is ~1×RTT +
max(download) instead of the ~N×RTT of one-at-a-time injection. The bootstrap
exposes window.VanillaGridReady, a Promise that resolves once every
feature has loaded (and rejects with the failing URL if any script errors).
Hosts must await window.VanillaGridReady (directly, or via the
<vn-grid> element's ready() method which delegates to it) before
instantiating a grid. Setting window.VanillaGridSequentialLoad = true
before vanilla-grid.js runs restores the previous strictly-sequential
injection as a temporary escape hatch. For synchronous bootstraps use the
pre-built vanilla-grid.bundle.js artifact (see below), which inlines every
feature.
Historical note: a
document.write-based synchronous path used to handle the parser-active case but was removed — it triggered warnings in every modern browser, blocked the parser on slow networks, and was incompatible with HTTP/2 server push and ad-blockers.
Auto-loading can be disabled by either of:
- setting
window.VanillaGridSkipAutoload = truebeforevanilla-grid.jsruns, or - adding
data-skip-autoloadto the<script src="vanilla-grid.js">tag.
TypeScript declarations
Hand-authored declaration file src/vanilla-grid/vanilla-grid.d.ts ships alongside the JavaScript sources. It covers:
- All constructor option types (
VanillaGridOptionsand every nested bag) VanillaGridclass with every public method and propertyVanillaGridElement(<vn-grid>) class with every public property, attribute, and method- Every
CustomEvent.detailshape keyed toVanillaGridEventsconstants (incl.vn-grid-sort-changed) VanillaGridStorageProviderand the three built-in subclassesVanillaGridDataManagerabstract base contractWindowglobal augmentations (VanillaGridReady,VanillaGridLogger,VanillaGridScheduler, etc.)HTMLElementTagNameMapextension sodocument.querySelector('vn-grid')infersVanillaGridElement
Usage (TypeScript project):
/// <reference path="path/to/vanilla-grid/vanilla-grid.d.ts" />
// or add to tsconfig.json:
// "typeRoots": ["node_modules/@types", "path/to/vanilla-grid"]
const grid = document.querySelector('vn-grid') as VanillaGridElement;
await grid.ready();
grid.addEventListener('vn-grid-loaded', (e: CustomEvent<VanillaGridLoadedDetail>) => {
console.log(e.detail.rows);
});
Pre-bundled artifact
The npm run build step produces dist/vanilla-components/latest/vanilla-grid/vanilla-grid.bundle.js, a single file that concatenates the storage provider, every feature, all data managers, vanilla-grid.js, and vanilla-grid-element.js in the correct order. The bundle declares window.VanillaGridSkipAutoload = true at the top, so the runtime auto-loader is a no-op and only one <script> tag is required:
<script src="vanilla-grid/vanilla-grid.bundle.js"></script>
Module load-order enforcement (build-time)
Every non-minified JS file in src/vanilla-grid/ carries Depends on: and Used by: header comments that make its evaluation-time requirements explicit:
// VanillaGridPersistenceFeature
// Depends on: storage/grid-storage-provider.js (VanillaGridStorageProvider)
// Used by: vanilla-grid.js (instantiates as this._persistence)
build.js reads these annotations at the start of every build and runs a topological sort (checkModuleLoadOrder()). The build fails immediately if:
- any declared dependency would be evaluated after the file that needs it (ordering violation), or
- a circular dependency is detected between any two modules.
This makes load-order constraints self-documenting and guarantees that BUNDLE_PARTS (and the auto-loader array in vanilla-grid.js) is always consistent with the declared dependency graph.
Constructor Architecture
VanillaGrid's constructor delegates to a fixed sequence of private _init*() helpers so each concern is independently readable and testable:
| Helper | Concern | Key dependency |
|---|---|---|
_initLogger(options) |
Normalises options.logger into a bound logger object |
— |
_initStrictMode(options) |
Resolves options.strict flag |
— |
_initStorage(options) |
Resolves storageMode + storageProvider |
_initLogger (logger passed to provider resolver) |
_initFormatting(options) |
Stores DOM entry-point refs (header, body, viewport …) and all formatting callbacks (locale, messages, formatInteger …) |
— |
_initLayout(options) |
Resolves row/header height, buffer row count, scrollbar, stretch-to-fit, scroll-speed multiplier | — |
_initSorting(options) |
Resolves sort options and column-reorder behaviour | — |
_initSelection(options) |
Resolves selection mode, key resolution, and checkbox options | — |
_initPersistence(options) |
Resolves persistence keys and write-debounce/flush policy; also resolves infinite-scroll options | — |
_initFeatures(options) |
Initialises all internal state variables; resolves derived DOM refs (bodyTable, container …); applies CSS custom properties; instantiates _persistence, _selection, _sorting, _columns; wires delegation bindings; resolves storage keys; kicks off the async persisted-state read |
All helpers above |
_initDomReferences() |
Creates bound handler references (boundScroll, boundWheel …) |
_initFeatures (prototype methods installed by feature files must be present) |
_initEventListeners() |
Attaches all addEventListener calls on viewport, document, window |
_initDomReferences (bound refs must exist) |
_initResizeObserver() |
Creates the ResizeObserver, calls _initCustomScrollbar(), and performs initial viewport-geometry measurement |
_initEventListeners (final setup step) |
The call order in the constructor is load-order-sensitive. The two non-obvious rules are:
- Logger before storage —
_resolveStorageProviderreceivesthis.loggeras a parameter. - All option resolution before
_initFeatures— the four feature constructors consume every field set by the preceding helpers via getter-function closures.
Host Integration Points (injectable adapters)
VanillaGrid's constructor accepts a few host-replaceable adapters so the same code path serves browsers, SSR, sandboxed iframes, and tests:
options.logger—{ debug, info, warn, error }. Defaults to a thin wrapper overconsole. Passfalse(ornull) to silence the grid completely. The<vn-grid>web component element also routes its own warnings through the grid instance logger (falling back to console before the grid is initialised). The normalisation helper is exposed aswindow.VanillaGridLogger.normalizeLogger(candidate)so host code andvanilla-grid-element.jscan reuse the same duck-typing logic;window.VanillaGridLogger.NOOP_LOGGERprovides the canonical silent-logger constant.options.strict—boolean(defaulttrue). Controls howsetColumns()handles malformed column definitions. Whentrue(the default), every misconfiguration is detected in a single-pass validator (_validateColumns()) and a descriptiveErroris thrown listing every violation, so developers see all problems at once. Whenfalse, the same checks emitlogger.warncalls instead of throwing, preserving the historically lenient behaviour for applications that need a gradual migration. Validated conditions include: non-object entries, missing/non-string keys, case-insensitive duplicate keys,frozen + hiddenon the same column, unknowntype, non-positivewidth/minWidth/maxWidth,minWidth > maxWidth, and non-functionrenderCell/valueGetter/sortFieldsfields.options.storageMode—'none' | 'local' | 'remote'(default'none'— opt-in). Selects the persistence back-end.'none'→ no settings are read or written. (Default.)'local'→ wrapswindow.localStorage(synchronous; falls back to in-memory when unavailable).'remote'→ usesoptions.storageProvider, an async provider subclassed fromVanillaGridRemoteStorageProvider. Reads must be awaited viagrid.ready(); writes are debounced and flushed in the background.
options.storageProvider— explicit provider instance. Always wins overstorageModewhen both are present.
Removed (May 2026): the legacy
options.storageWeb-Storage-like adapter ({ getItem, setItem, removeItem, hasItem? }) is no longer accepted. Hosts that previously passedoptions.storageshould switch tooptions.storageProvider(orstorageMode: 'local' | 'remote').
Static, host-wide configuration (call once at startup, before constructing grids):
VanillaGrid.defaultLocale = 'it-IT'— overrides the global fallback locale (default'en-US').VanillaGrid.registerLocale('it', { … })— registers/replaces a message bundle. The grid resolves the longest matching prefix (e.g.'en-US'→'en').VanillaGrid.getLocaleMessages('en')— inspect a registered bundle.VanillaGridElement.registerTheme('corporate', '/themes/grid-corporate.css')— adds a named theme so thethemeattribute accepts it like a built-in.VanillaGridElement.getSupportedThemes()— returns built-in + registered theme names.
See Localization Implementation, Themes Implementation, and Local Storage Settings Implementation for full details.
Public API — Return-type contract
All public methods on VanillaGrid and <vn-grid> (VanillaGridElement) follow these invariants:
| Shape | Rule |
|---|---|
| "Not configured" | Return undefined — the value was never set or is not applicable. Example: getTotalRowCount() returns undefined when the total row count is unknown. |
| "Explicitly cleared" | Return null — the host passed null / called a clear operation. |
| Collection getters | Always return a freshly-allocated Array — never expose an internal Set or Map reference. Examples: getSelectedKeys(), getFrozenColumns(), getHiddenColumns(). |
| Boolean getters | Always return a strict true or false — never undefined or a truthy/falsy non-boolean. Examples: isFrozenColumn(), canHideColumn(), isRowSelected(), hasActiveSort(). |
| Setters | Return void — no fluent/chainable API unless explicitly documented. Examples: setDataManager(), setAutoReloadOnConfigChange(), setStorageProvider(). |
isRowSelected(row) on <vn-grid>
VanillaGridElement now exposes isRowSelected(row) as a first-class public method, mirroring the same method on the internal VanillaGrid instance. Returns false when the grid has not been initialized yet (safe to call before initializeGrid()).
Mobile / touch integration
Touch-platform hardening distinct from scroll-performance tuning (see Row Virtualization and Custom Scrollbars for the momentum-scrolling side). What the component guarantees vs. what the host page owes:
Guaranteed by the component:
- No text-selection/callout on chrome. Header cells (
.vn-grid-header-table th) and body cells (.vn-grid-body-table td) areuser-select: none(with the-webkit-user-selecttwin for pre-16.4 iOS Safari, which ignores the unprefixed property)..vn-grid-table-containersets-webkit-touch-callout: noneso a long-press over customrenderCellcontent (links, images) never shows the iOS Copy/Look-Up sheet. Body cells can opt back into selectable text (e.g.renderCelloutput with IDs/emails users legitimately copy) by setting--vn-grid-cell-user-select: texton.vn-grid-table-containerfor that grid instance — the property has no effect on header cells, which are never user-copyable content. - No tap-highlight flash / double-tap zoom.
-webkit-tap-highlight-color: transparenton the container suppresses the platform tap rectangle; header cells carrytouch-action: manipulationso a fast double-tap on the header never triggers browser zoom. - Viewport overscroll containment.
.vn-grid-virtual-list-viewportsetsoverscroll-behavior: none, which stops scroll chaining to the document for gestures that start inside the viewport, on engines that support it (iOS Safari 16+, all current Chromium/Firefox). It cannot do anything for gestures that start on the header, toolbar, or page padding — those pan the document scroller directly and are outside the component's DOM. - No input focus-zoom inside the component. The column-filter panel's
operator/value inputs (
.vn-grid-filter-operator,.vn-grid-filter-value) raise their font-size tomax(current, 16px)under@media (pointer: coarse)— iOS Safari auto-zooms the page when a focused input's computed font-size is below 16px, and the zoom persists after blur until the user pinches back out. The toolbar's search input has the equivalent fix; see vanilla-grid-toolbar.
Owed by the host page: the component cannot suppress document-level
rubber-band bounce or pull-to-refresh from inside — that requires styling
html/body, which a well-behaved web component must never do on its own
(it would break embedders that want PTR elsewhere on the page). A
full-viewport grid app needs one line of host CSS:
html, body {
overscroll-behavior-y: none; /* stops PTR on Chrome/Edge Android and
chaining/PTR on iOS Safari 16+ */
}
plus a fixed app shell — height: 100dvh on body (the modern viewport
unit; see people-cities-js through wikipedia-pages-vue) is the recommended
pattern. overscroll-behavior is silently ignored on iOS Safari < 16, so
hosts that must support it need the classic fallback instead:
position: fixed; inset: 0; on the app shell, which prevents any document
panning at all regardless of engine support.
iOS-version caveats: unprefixed user-select: none is honored from iOS
Safari 16.4 (March 2023) — every earlier version needs the
-webkit-user-select twin the component already ships. overscroll-behavior
is supported from iOS Safari 16 — earlier versions ignore both the
component's viewport rule and the host's html/body rule; the
position: fixed shell is the only way to fully cover them.
Suggested Reading Order
The Documents list above is already ordered: read the nine groups in sequence and each one only depends on the groups before it. Group 1 explains what the element is and how rows reach the screen; group 2 explains where rows come from; groups 3–5 are the features that operate on those rows and columns; groups 6–8 layer presentation, export, and persistence on top; group 9 (row grouping) depends on group 1's render pipeline and group 3's sort machinery.
If you only need enough context to work on one feature, read this short path first:
- Web Component Implementation — how
<vn-grid>wraps and exposes the core grid. - Row Virtualization and Custom Scrollbars — the rendering and scroll engine.
- DataManager Implementation — the data access contract and lifecycle wiring.
- Row Key Management — stable row identity, which selection and re-render both depend on.
- Themes Implementation — the CSS variable system every feature's UI is styled through.
Then jump straight to the document for the feature you are changing.