Skip to content

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.


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:

PartMeaning
[tbw-grid…]Came from the grid, not your app code
#employeesThe grid’s id attribute (or omitted if anonymous) — useful when you have several on a page
:SelectionPluginThe plugin that produced the message (omitted for core messages)
TBW020Stable 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 needed
await grid.ready(); // wait for first render
// Public — frozen, async, safe to assign from production code
const 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 state
console.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 data

Common 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.columns after assignment — did your inferred columns end up where you expected?
  • grid.rows.length vs grid.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 lookup
const 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 versions
grid.querySelector(GridSelectors.HEADER_ROW); // header row
grid.querySelectorAll(GridSelectors.DATA_ROW); // all rendered data rows
grid.querySelectorAll(GridSelectors.DATA_CELL); // all rendered cells
grid.querySelectorAll(GridSelectors.SELECTED_ROWS); // selected rows
grid.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' column
grid.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 element
grid.findRenderedRowElement(42); // null if row 42 is outside the virtualized window
grid._hostElement; // the grid root element
grid._renderRoot; // the inner render container

Virtualization 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 in grid.rows but hasn’t been rendered yet — call grid.scrollToRow(rowIndex) (or grid.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 change
grid.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 RENDER after every data mutation; if it doesn’t fire, the grid isn’t seeing the change (most often: you mutated rows in place instead of assigning a new array).
  • “Selection state is wrong” → log SELECTION_CHANGE to 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.

Plugin issues usually trace to one of three causes:

  1. Plugin not attached. Confirm via (await grid.getConfig()).plugins. If your column uses editable: true and you see no EditingPlugin in the list, you forgot the feature import.
  2. Hook order conflict. Two plugins manipulating the same columns/rows — see Two plugins modifying the same columns/rows.
  3. Hook too slow. Plugin work in afterCellRender / afterRowRender shows 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,
})));

Stable APIs (covered by semver):

APIReturnsUse 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.rowscurrent arraysAfter-the-fact inspection
grid.sourceRowsunfiltered/unsorted dataDistinguish view from source
grid.getRow(id) / grid.getRowId(row)row / stringRow identity debugging
grid.on(name, listener)disposerWatch what the grid is doing
DGEvents / PluginEventsevent-name catalog”What can I even listen for?”
GridSelectors / GridClasses / GridDataAttrsstable selectors and attribute namesStandard DOM queries against the grid
grid.scrollToRow(index) / grid.scrollToRowById(id)voidBring 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):

APIReturnsUse when
grid.effectiveConfigGridConfig (sync, live)Quick sync peek at merged config from the console
grid.findHeaderRow()HTMLElementDirect header lookup
grid.findRenderedRowElement(index)element or nullDOM lookup respecting virtualization
grid._pluginManagerPluginManagerInspect plugin ordering, hook subscriptions
grid._virtualization{ start, end, … }See the current virtual window
grid._hostElement / grid._renderRootHTMLElementDirect internal DOM access
grid.disconnectSignalAbortSignalAuto-cleanup for ad-hoc listeners

The grid virtualizes rows for performance, which requires a constrained height. This is the most common setup issue.

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 };

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;
}}

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 },
};

If first paint feels slow, disable animations:

grid.gridConfig = { animation: { mode: 'off' } };

See the Performance Guide for profiling techniques and detailed tuning.


The sections below cover issues specific to custom plugin and framework adapter development.

A plugin requires another plugin that isn’t loaded:

UndoRedoPlugin requires EditingPlugin

Fix with features API (auto-resolves dependencies):

import '@toolbox-web/grid/features/undo-redo';
// EditingPlugin is loaded automatically

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

Plugin APlugin BIssue
GroupingRowsPluginTreePluginBoth transform the entire row model
GroupingRowsPluginPivotPluginPivot creates its own row/column structure
TreePluginPivotPluginPivot replaces the data structure
ServerSidePluginGroupingRows/Tree/PivotThese require the full dataset; server-side loads lazily

See the Plugins Overview → Known Incompatibilities for the full list.

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.

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.


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:

  1. 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.
  2. Capture the diagnostic output — copy the TBW error code and full prefixed message from the console.
  3. Capture the effective configconsole.log(await grid.getConfig()) and include the plugin list.
  4. Search existing GitHub issues — someone may have hit the same wall.
  5. Open a new issue with reproduction, browser/OS, grid version, and the captures above.
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://toolboxjs.com/llms-full.txt