Troubleshooting
The grid is designed to work out of the box with minimal setup. If you’re hitting issues with basic configuration — TypeScript types, framework imports, or event handling — check the Getting Started guide first, which covers setup for all frameworks.
This page covers two things: how to debug the grid when something is wrong, and the genuine gotchas that can’t be solved in code alone.
How to debug the grid
Section titled “How to debug the grid”When the grid isn’t doing what you expect, work through this checklist in order. Most issues fall out at one of the first three steps.
1. Read the console — every warning has a code
Section titled “1. Read the console — every warning has a code”Every user-facing warning and error from the grid is prefixed with [tbw-grid#<your-id>:<PluginName>] and tagged with a stable diagnostic code (e.g. TBW001, TBW020). The code → cause mapping is published on the Error Codes reference page.
[tbw-grid#employees:SelectionPlugin] TBW020 SelectionPlugin requires EditingPlugin when `editable` is set on any column. Add to your features: import '@toolbox-web/grid/features/editing';What the parts mean:
| Part | Meaning |
|---|---|
[tbw-grid…] | Came from the grid, not your app code |
#employees | The grid’s id attribute (or omitted if anonymous) — useful when you have several on a page |
:SelectionPlugin | The plugin that produced the message (omitted for core messages) |
TBW020 | Stable code — links to the errors page |
If a TBW code appears in production logs or a Sentry event, search the errors page first — most have an actionable fix one click away.
2. Inspect the grid’s effective configuration
Section titled “2. Inspect the grid’s effective configuration”The biggest source of “why isn’t this working?” confusion is what config did the grid actually end up with, after defaults, features, plugin merges, and light-DOM column nodes are all combined. The grid exposes the resolved config two ways:
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('#employees'); // typed DataGridElement<T> — no casts neededawait grid.ready(); // wait for first render
// Public — frozen, async, safe to assign from production codeconst config = await grid.getConfig();console.log(config);console.log(config.plugins);
// Internal — synchronous getter, the live merged config. Great in the DevTools// console; do NOT call from production code (see stability note below).console.log(grid.effectiveConfig);console.log(grid.effectiveConfig.plugins);
// Always-public stateconsole.log(grid.columns); // current column array (post-plugin transforms)console.log(grid.rows); // current rows (post-sort/filter)console.log(grid.sourceRows); // unfiltered, unsorted source dataCommon things to check here:
config.plugins?.map(p => p.constructor.name)— is the plugin you expect actually attached? Plugins are silently not added if their feature import was missed.grid.columnsafter assignment — did your inferred columns end up where you expected?grid.rows.lengthvsgrid.sourceRows.length— does filtering/grouping account for the difference?grid._pluginManager(internal) — exposes the live plugin manager (lookup, ordering, hook subscriptions).grid._virtualization(internal) — current virtual window:{ start, end, … }— useful for “why isn’t row N in the DOM?“.
3. Find your grid (and a specific row/cell) in the DOM
Section titled “3. Find your grid (and a specific row/cell) in the DOM”The grid uses Light DOM — there’s no shadow root to break inspection. Standard DevTools “Inspect element” works directly. The grid publishes its class names, data-attribute names, and prebuilt selectors as public constants so your queries don’t break if internals change:
import { GridSelectors, GridClasses, GridDataAttrs, queryGrid } from '@toolbox-web/grid';
// Multi-version-safe grid lookupconst grid = queryGrid('#employees');// Or any grid on the page (works across versions thanks to the data attribute):queryGrid('[data-tbw-grid]');
// Use the published selectors — these are stable across versionsgrid.querySelector(GridSelectors.HEADER_ROW); // header rowgrid.querySelectorAll(GridSelectors.DATA_ROW); // all rendered data rowsgrid.querySelectorAll(GridSelectors.DATA_CELL); // all rendered cellsgrid.querySelectorAll(GridSelectors.SELECTED_ROWS); // selected rowsgrid.querySelector(GridSelectors.EDITING_CELL); // the currently-editing cell
// Find a specific cell by field, or by row index (data-row is on the cell)grid.querySelector(GridSelectors.CELL_BY_FIELD('email')); // first cell in the 'email' columngrid.querySelector(`.${GridClasses.DATA_CELL}[data-row="42"][${GridDataAttrs.FIELD}="email"]`);Internal shortcuts that are convenient in the DevTools console (subject to the stability note above):
grid.findHeaderRow(); // the header row elementgrid.findRenderedRowElement(42); // null if row 42 is outside the virtualized windowgrid._hostElement; // the grid root elementgrid._renderRoot; // the inner render containerVirtualization gotcha: at any moment the DOM only contains rows in the visible viewport + a small overscan buffer. If
grid.querySelectorAll(GridSelectors.DATA_ROW)doesn’t include the row you expect, it exists ingrid.rowsbut hasn’t been rendered yet — callgrid.scrollToRow(rowIndex)(orgrid.scrollToRowById(rowId)) to bring it into view, or operate on the data, not the DOM.
4. Watch what the grid is doing — listen to lifecycle events
Section titled “4. Watch what the grid is doing — listen to lifecycle events”Most “is the grid even re-rendering?” / “did my sort actually apply?” questions are answered in five seconds by attaching a listener. The full event catalog is exported as DGEvents:
import { DGEvents, PluginEvents } from '@toolbox-web/grid';
// Log every render (use the string literal — `render` is not in the legacy// DGEvents enum because it was added later; the listener is type-checked// against `DataGridEventMap`).grid.on('render', (detail) => console.log('render', detail));
// Log every sort / filter / column / selection changegrid.on(DGEvents.SORT_CHANGE, (detail) => console.log('sort', detail));grid.on(PluginEvents.FILTER_CHANGE, (detail) => console.log('filter', detail));grid.on(DGEvents.COLUMN_STATE_CHANGE, (detail) => console.log('columns', detail));grid.on(PluginEvents.SELECTION_CHANGE, (detail) => console.log('selection', detail));
// Log everything (catch-all — use briefly)for (const name of [...Object.values(DGEvents), ...Object.values(PluginEvents)]) { grid.on(name, (detail) => console.log(name, detail));}grid.on() returns a disposer (() => void) — call it to stop listening. Useful patterns:
- “My edit isn’t saving” → listen for
'cell-commit'/'row-commit'and'edit-close'; see which one fires. - “My data appears stale” → log
RENDERafter every data mutation; if it doesn’t fire, the grid isn’t seeing the change (most often: you mutatedrowsin place instead of assigning a new array). - “Selection state is wrong” → log
SELECTION_CHANGEto see what the grid thinks is selected.
5. Diagnose a hot path with the Performance tab
Section titled “5. Diagnose a hot path with the Performance tab”Anything that works but is slow is a performance question, not a config question. The Performance guide has a flame-graph reading walkthrough that distinguishes grid-internal work from framework reconciliation from layout thrashing. Start there.
6. Diagnose a plugin
Section titled “6. Diagnose a plugin”Plugin issues usually trace to one of three causes:
- Plugin not attached. Confirm via
(await grid.getConfig()).plugins. If your column useseditable: trueand you see noEditingPluginin the list, you forgot the feature import. - Hook order conflict. Two plugins manipulating the same columns/rows — see Two plugins modifying the same columns/rows.
- Hook too slow. Plugin work in
afterCellRender/afterRowRendershows up in the flame graph during scroll. See Hook performance budget.
When filing a plugin bug, include the output of:
const config = await grid.getConfig();console.log(config.plugins?.map(p => ({ name: p.constructor.name, config: p,})));Quick reference: debug APIs
Section titled “Quick reference: debug APIs”Stable APIs (covered by semver):
| API | Returns | Use when |
|---|---|---|
queryGrid(selector) | grid element (or null) | Multi-version-safe lookup |
grid.ready() | Promise<void> | Wait for first render before assertions |
grid.getConfig() | Promise<Readonly<GridConfig>> | ”What did the grid actually merge?” — async, frozen |
grid.columns / grid.rows | current arrays | After-the-fact inspection |
grid.sourceRows | unfiltered/unsorted data | Distinguish view from source |
grid.getRow(id) / grid.getRowId(row) | row / string | Row identity debugging |
grid.on(name, listener) | disposer | Watch what the grid is doing |
DGEvents / PluginEvents | event-name catalog | ”What can I even listen for?” |
GridSelectors / GridClasses / GridDataAttrs | stable selectors and attribute names | Standard DOM queries against the grid |
grid.scrollToRow(index) / grid.scrollToRowById(id) | void | Bring a virtualized row into view |
grid.forceLayout() | Promise<void> | Re-measure after content change |
Internal helpers (reachable from the DevTools console — not covered by semver):
| API | Returns | Use when |
|---|---|---|
grid.effectiveConfig | GridConfig (sync, live) | Quick sync peek at merged config from the console |
grid.findHeaderRow() | HTMLElement | Direct header lookup |
grid.findRenderedRowElement(index) | element or null | DOM lookup respecting virtualization |
grid._pluginManager | PluginManager | Inspect plugin ordering, hook subscriptions |
grid._virtualization | { start, end, … } | See the current virtual window |
grid._hostElement / grid._renderRoot | HTMLElement | Direct internal DOM access |
grid.disconnectSignal | AbortSignal | Auto-cleanup for ad-hoc listeners |
Height & Virtualization
Section titled “Height & Virtualization”The grid virtualizes rows for performance, which requires a constrained height. This is the most common setup issue.
Grid appears empty or collapsed
Section titled “Grid appears empty or collapsed”The grid needs an explicit height. Without one, the container collapses to zero and nothing renders.
/* ✅ Fixed height */tbw-grid { height: 500px;}
/* ✅ Flex layout */.container { display: flex; flex-direction: column; height: 100vh;}tbw-grid { flex: 1; min-height: 0; /* Required for flex children to shrink */}If you don’t need virtual scrolling (small datasets), set height: auto so the grid sizes to its content:
tbw-grid { height: auto;}Rows appear in wrong positions after scroll
Section titled “Rows appear in wrong positions after scroll”Variable row heights can cause position drift. Use a fixed rowHeight for best accuracy:
grid.gridConfig = { rowHeight: 36 };If you need auto-measured heights, call forceLayout() after content changes:
await grid.forceLayout();Blank rows or flickering during fast scroll
Section titled “Blank rows or flickering during fast scroll”The grid renders a buffer of extra rows (overscan) above and below the viewport. During very fast scrolling, the buffer can be exhausted before new rows paint. Setting rowHeight explicitly lets the grid calculate positions without measuring, which keeps up better:
grid.gridConfig = { rowHeight: 32 };Performance
Section titled “Performance”Use formatters over renderers for simple values
Section titled “Use formatters over renderers for simple values”format returns a string — significantly faster than creating DOM elements in a renderer:
// ✅ Fast — string formatter{ field: 'salary', format: (v) => `$${v.toLocaleString()}` }
// ⚠️ Slower — DOM renderer (use only when you need rich content){ field: 'salary', renderer: (ctx) => { const el = document.createElement('span'); el.textContent = `$${ctx.value.toLocaleString()}`; return el;}}Column virtualization for wide grids
Section titled “Column virtualization for wide grids”For 50+ columns, enable the columnVirtualization feature so off-screen columns aren’t rendered:
import '@toolbox-web/grid/features/column-virtualization';
grid.gridConfig = { features: { columnVirtualization: true },};Disable animations during initial load
Section titled “Disable animations during initial load”If first paint feels slow, disable animations:
grid.gridConfig = { animation: { mode: 'off' } };See the Performance Guide for profiling techniques and detailed tuning.
For Plugin & Adapter Developers
Section titled “For Plugin & Adapter Developers”The sections below cover issues specific to custom plugin and framework adapter development.
Plugin dependency errors
Section titled “Plugin dependency errors”A plugin requires another plugin that isn’t loaded:
UndoRedoPlugin requires EditingPluginFix with features API (auto-resolves dependencies):
import '@toolbox-web/grid/features/undo-redo';// EditingPlugin is loaded automaticallyFix with plugins API (manual ordering):
plugins: [ new EditingPlugin(), // Must come first new UndoRedoPlugin(), // Depends on EditingPlugin]Two plugins modifying the same columns/rows
Section titled “Two plugins modifying the same columns/rows”Symptoms: Unexpected column order, missing columns, or data appearing twice.
Plugins process columns and rows in registration order. If two plugins manipulate the same columns, they may conflict. When using the features API, try reordering the features entries. When using the plugins API, reorder the array — the last plugin to process wins.
Known plugin incompatibilities
Section titled “Known plugin incompatibilities”| Plugin A | Plugin B | Issue |
|---|---|---|
| GroupingRowsPlugin | TreePlugin | Both transform the entire row model |
| GroupingRowsPlugin | PivotPlugin | Pivot creates its own row/column structure |
| TreePlugin | PivotPlugin | Pivot replaces the data structure |
| ServerSidePlugin | GroupingRows/Tree/Pivot | These require the full dataset; server-side loads lazily |
See the Plugins Overview → Known Incompatibilities for the full list.
Styles not applying (CSP)
Section titled “Styles not applying (CSP)”The grid injects plugin styles via adoptedStyleSheets. If your CSP blocks this, styles fail silently.
Required CSP header:
style-src 'self';adoptedStyleSheets does not require 'unsafe-inline' — it’s treated as a programmatic stylesheet. Check the browser console for CSP violation messages.
CSS cascade layer specificity
Section titled “CSS cascade layer specificity”The grid’s built-in styles use CSS @layer. Your custom CSS may not be specific enough to override them.
Use CSS custom properties (recommended):
tbw-grid { --tbw-color-row-hover: #e0f7fa; --tbw-cell-padding: 12px 16px;}Since the grid uses Light DOM, normal CSS selectors also work — just ensure your selectors are specific enough to beat the @layer defaults.
Still Stuck?
Section titled “Still Stuck?”If you’ve worked through How to debug the grid and the issue isn’t covered above, the fastest path to help is a minimal reproduction:
- Strip your code to the smallest example that still reproduces the issue — one grid, the minimum config, ideally in a StackBlitz or a single HTML file with the UMD bundle.
- Capture the diagnostic output — copy the TBW error code and full prefixed message from the console.
- Capture the effective config —
console.log(await grid.getConfig())and include the plugin list. - Search existing GitHub issues — someone may have hit the same wall.
- Open a new issue with reproduction, browser/OS, grid version, and the captures above.