Skip to content

Column Virtualization Plugin

The Column Virtualization plugin improves performance for grids with many columns.

import '@toolbox-web/grid/features/column-virtualization';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/column-virtualization';
const grid = queryGrid('tbw-grid');
grid.gridConfig = {
columns: generateManyColumns(100), // Your column generator
fitMode: 'fixed', // Required for column virtualization
features: { columnVirtualization: true },
};
grid.rows = data;
Column countNumber of columns to generate
ThresholdMin columns to enable virtualization
OverscanExtra columns rendered offscreen
Auto-enableAutomatically enable when column count exceeds threshold

Use the Column Count slider to add up to 1000 columns, and toggle Auto Enable to compare performance with and without virtualization.

See ColumnVirtualizationConfig for the full list of options and defaults. By default the plugin activates itself once a grid exceeds 30 columns; set autoEnable: false to control activation yourself.

  • fitMode: 'fixed' — the plugin is designed for fixed-width column layouts
  • Columns should have explicit widths (columns without widths default to 100px)

Pinned columns and utility columns (selection checkbox, master-detail expander, row-drag handle) are never virtualized away: they are always rendered, so pinned ones stay visible while you scroll horizontally. Unpinned utility columns scroll out of view like any unpinned column; pin them to keep them on screen.

Virtualization only limits which columns are rendered. grid.columns and every column API (setColumnVisible(), pinning, reordering, …) keep working on the full column list, including the columns scrolled out of view. The Export and Clipboard plugins therefore include every column when you export or copy without a cell selection, or pass columns explicitly.

Selection ranges, the selection anchor, and the col of every CellRange index the full list of visible columns, not the rendered window. A selection stays on its columns while the grid scrolls horizontally, selectAll() (Ctrl+A) covers every column, and copying or pasting a selection uses the selected columns even when they are scrolled out of view. Column-mode selection (selectColumn(), selectAllColumns(), getSelectedColumns()) works on columns outside the window too.

The focused cell also stays on its column. When you scroll it out of view, no cell shows the focus ring and grid.focusedCell.colIndex is -1, while grid.focusedCell.field still names the column. The next key press on the grid or one of its cells scrolls the column back into view before it is handled, as in a spreadsheet. Keys typed into a control (an input or button rendered in a header or cell) leave it scrolled out. Home / End go to the first / last column of the whole grid, not of the rendered window.

Arrow keys and Tab step through every column: they scroll the next column into view instead of stopping at the rendered window’s edge or jumping into a pinned column.

A paste without a multi-cell selection writes into consecutive columns of the full list, so a wide paste continues past the rendered window instead of stopping at its edge. A selected range pastes into the columns it selects.

Rows with open editors follow horizontal scrolling like other rows. An editor whose column stays in view keeps its element, focus and draft, and columns that scroll into view get editors (in mode: 'grid', or on the row being edited in row mode). A focused editor whose column scrolls out of view is blurred first, which commits its value. Tab in an editor continues to the next editable column even when it is outside the rendered window, and beginCellEdit() scrolls an off-window column into view before opening its editor.

The rendered window is computed after every other column plugin (pinning, grouping, pivot), whatever their order in plugins, from their final output. Pinned order, group header rows, and pivot columns are therefore preserved.

A custom plugin that maps columns onto rendered DOM cells by index should read this.visibleColumns (the rendered columns) rather than this.columns (the full list).

const plugin = grid.getPluginByName('columnVirtualization');
// Check if virtualization is currently active
const active = plugin.getIsVirtualized();
// Get the visible column index range
const { start, end } = plugin.getVisibleColumnRange();
// Scroll a specific column into view
plugin.scrollToColumn(42);